Fantas, Eel, and Specification 8: Apply
10 Apr 2017Aaand we’re back - hello, everyone! Today, we’re going to take another look at those mystical Functor
types. We said a couple weeks ago that functors encapsulate a little world (context) with some sort of language extension. Well, what happens when worlds collide? Let’s talk about Apply
.
All Apply
types are Functor
types by requirement, so we know they’re definitely “containers” for other types. The exciting new feature here is this ap
function:
ap :: Apply f => f a ~> f (a -> b) -> f b
-- a -> (a -> b) -> b
If we ignore the f
s, we get the second line, which is our basic function application: we apply a value of type a
to a function of type a -> b
, and we get a value of type b
. Woo! What’s the difference with ap
? All those bits are wrapped in the context of our f
functor!
That’s the, uh, “grand reveal”. Ta-da. I’m not really sure that, in isolation, this particularly helps our intuition, though, so let’s instead look at ap
within the lift2
function:
// Remember: `f` MUST be curried!
// lift2 :: Applicative f
// => (a -> b -> c)
// -> f a -> f b -> f c
const lift2 = f => a => b =>
b.ap(a.map(f))
For me, this is much clearer. lift2
lets us combine two separate wrapped values into one with a given function.
lift1
, if you think about it, is justa.map(f)
. Thelift2
pattern actually works for any number of arguments; once you finish the article, why not try to writelift3
? Orlift4
?
Wait, combine? Do I sense a Semigroup
?
Sort of! You can think of it this way: a Semigroup
type allows us to merge values. An Apply
type allows us to merge contexts. Neat, huh? Now, how could we forget the laws?!
// compose :: (b -> c) -> (a -> b) -> a -> c
const compose = f => g => x => f(g(x))
// COMPOSITION LAW
x.ap(g.ap(f.map(compose))) === x.ap(g).ap(f)
// But, if we write lift3...
const lift3 =
f => a => b => c =>
c.ap(b.ap(a.map(f)))
// Now our law looks like this!
lift3(compose)(f)(g)(x) === x.ap(g).ap(f)
// Remember: x.map(compose(f)(g))
// === x.map(g).map(f)
By introducing some little helper functions, our law seems much clearer, and a little more familiar. It says that, just as map
could only apply a function to the wrapped value, ap
can only apply a wrapped function to the wrapped value. No magic tricks!
Before we go any further, I challenge you take a moment to try to build lift2
without ap
. Just think about why we couldn’t do this with a plain old Functor
. If we tried to write lift2
, we might end up here:
// lift2F :: Functor f
// => ( a -> b -> c)
// -> f a -> f b -> f (f c)
const lift2F = f => as => bs =>
as.map(a => bs.map(b => f(a)(b)))
So, we can apply the inner values to our function - hooray! - but look at the type here. We’re doing a map
inside a map
, so we’ve ended up with two levels of our functor type! It’s clear that we can’t write a generic lift2
to work with any Functor
, and ap
is what’s missing.
With all that out the way, let’s look at some examples, shall we? We’ll start with the Identity
type’s ap
from our beloved spec:
const Identity = daggy.tagged('Identity', ['x'])
// map :: Identity a ~> (a -> b)
// -> Identity b
Identity.prototype.map = function (f) {
return new Identity(f(this.x))
}
// ap :: Identity a ~> Identity (a -> b)
// -> Identity b
Identity.prototype.ap = function (b) {
return new Identity(b.x(this.x))
}
// Identity(5)
lift2(x => y => x + y)
(Identity(2))
(Identity(3))
No frills, no magic. Identity.ap
takes the function from b
, the value from this
, and returns the wrapped-up result. Did you spot the similarity in type between map
and ap
, by the way? Moving on, here’s the slightly more complex implementation for Array
:
// Our implementation of ap.
// ap :: Array a ~> Array (a -> b) -> Array b
Array.prototype.ap = function (fs) {
return [].concat(... fs.map(
f => this.map(f)
))
}
// 3 x 0 elements
// []
[2, 3, 4].ap([])
// 3 x 1 elements
// [ '2!', '3!', '4!' ]
[2, 3, 4]
.ap([x => x + '!'])
// 3 x 2 elements
// [ '2!', '3!', '4!'
// , '2?', '3?', '4?' ]
[2, 3, 4]
.ap([ x => x + '!'
, x => x + '?' ])
I’ve put a little note with the answers so we can see what’s happening: we get every a
and b
pair. This is called the cartesian product of the two arrays. On top of that, when we lift2
an f
over two Array
types, we’re actually doing something quite familiar…
return lift2(x => y => x + y)(array1)(array2)
// ... is the same as...
const result = []
for (x in array1)
for (y in array2)
result.push(x + y)
return result
We get a really pretty shorthand for multi-dimensional loops. Flattening a loop within a loop gives us every possible pair of elements, and that’s what ap
is for! If this feels weird, just think of the types. We have to use Array (a -> b)
and Array a
to get to Array b
without violating the composition law; there aren’t many possibilities!
There are loads of types with ap
instances. Most, we’ll see, implement ap
in terms of chain
; we’ll look at the Chain
spec in a week or two, so don’t worry too much. Most of them are fairly intuitive anyway:
Maybe
combines possible failures. If either of the twoMaybe
values areNothing
, the result isNothing
.
Just(2).ap(Just(x => -x)) // Just(-2)
Nothing.ap(Just(x => -x)) // Nothing
Just(2).ap(Nothing) // Nothing
Nothing.ap(Nothing) // Nothing
Either
combines possible failures with exceptions. If either of the two areLeft
, the result is the firstLeft
.
Right(2) .ap(Right(x => -x)) // Right(-2)
Left('halp').ap(Right(x => -x)) // Left('halp')
Right(2) .ap(Left('eek')) // Left('eek')
Left('halp').ap(Left('eek')) // Left('eek')
At some point, I’d like to write a follow up to the Functor
post to give some more practical examples, but, for now, this is hopefully understandable (please tweet me if I’m wrong!). Whatever your Functor
trickery, ap
is map
with a wrapped function. Before we go, though, I’d like to talk about one last trick up Apply
’s sleeve…
A type we haven’t talked about before is Task
. This is similar to Either
- it represents either an error or a value - but the difference is that Task
’s value is the result of a possibly-asynchronous computation. They look a lot like Promise
types:
const Task = require('data.task')
// Convert a fetch promise to a Task.
const getJSON = url => new Task((rej, res) =>
fetch(url).then(res).catch(rej))
We can see that it holds a function that will eventually call a resolver. Task
, just like Promise
, sorts out all the async wiring for us. However, an interesting feature of Task
is that it implements Apply
. Let’s take a look:
const renderPage = users => posts =>
/* Write some HTML with this data... */
// A Promise of a web page.
// page :: Task e HTML
const page =
lift2(renderPage)
(getJSON('/users'))
(getJSON('/posts'))
Just as we’d expect: we get the two results, and combine them into one using renderPage
as the “glue”. However, we can see that lift2
’s second and third arguments have no dependencies on one another. Because of this, the arguments to lift2
can always be calculated in parallel. Do you hear that? These AJAX requests are automatically parallelised! Ooer!
You can see Task.ap
’s implementation for an exact explanation, but isn’t this great? We can abstract parallelism and never have to worry about it! When we have two parallel Task
s and finally want to glue them back together, we just use lift2
! Parallelism becomes an implementation detail. Out of sight, out of mind!
I think Task
gives a really strong case for Apply
and why it’s immediately useful. When we look at Traversable
in a few weeks, we’ll come back to ap
and see just how powerful it is. Until then, don’t overthink ap
- it’s just a mechanism for combining contexts (worlds!) together without unwrapping them.
I had originally intended to mention of
in this post and cover the full Applicative
. However, it’s already quite a long post, so I’ll write up that post some time this week! I might even throw in some bigger practical examples for good measure.
If you’re still with me, hooray! I hope that wasn’t too full-on. We’re definitely wading in deeper waters now, getting to the more advanced parts of the spec. All the more reason to keep asking questions, though! I want to make this as clear as possible, so don’t hesitate to get in touch.
For now until we talk about Applicative
, though, it’s goodbye from me! Keep at it, Apply
yourself (zing - this blog has jokes now!), and take care ♥