Fantas, Eel, and Specification 9: Applicative
17 Apr 2017I asked my German friend whether any of this series’ posts particularly stood out. They said 9, so I’d better make this a good one! I told you we were doing jokes now, right? Moving on… Today, we’re going to finish up a topic we started last week and move from our Apply
types to Applicative
. If you understood the Apply post, this one is hopefully going to be pretty intuitive. Hooray!
Applicative
types are Apply
types with one extra function, which we define in Fantasy Land as of
:
of :: Applicative f => a -> f a
With of
, we can take a value, and lift it into the given Applicative
. That’s it! In the wild, most Apply
types you practically use will also be Applicative
, but we’ll go through a counter-example later on! Anyway, wouldn’t you know, there are a few laws to go with it, tying ap
and of
together:
// For some applicative A...
// Identity.
v.ap(A.of(x => x)) === v
// Homomorphism
A.of(x).ap(A.of(f)) === A.of(f(x))
// Interchange
A.of(y).ap(u) === u.ap(A.of(f => f(y)))
With identity, we’ve lifted the identity function (x => x
) into our context, applied it to the inner value, and, surprise, nothing happened.
The homomorphism law says that we can lift a function and its argument separately and then combine them, or combine them and then lift the result. Either way, we’ll end up with the same answer! The of
function can do nothing but put the value into the Applicative
. No tricks. No side effects.
Interchange is a little more complex. We can lift y
and apply it to the function in u
, or we can lift f => f(y)
, and apply u
to that. I think of this one as, “Nothing special happens to one particular side of ap
- it just applies the value in the right side to the value in the left”.
So, why is this of
thing useful? Well, the most exciting reason is that we can write functions generic to any applicative. Let’s write a function that takes a list of applicative-wrapped values, and returns an applicative-wrapped list of values. Note that T
is a TypeRep
variable - we’ve seen these before in the monoid post. We need it in case the list is empty:
// append :: a -> [a] -> [a]
const append = y => xs => xs.concat([y])
// There's that sneaky lift2 again!
// lift2 :: Applicative f
// => (a -> b -> c, f a, f b)
// -> f c
const lift2 = (f, a, b) => b.ap(a.map(f))
// insideOut :: Applicative f
// => [f a] -> f [a]
const insideOut = (T, xs) => xs.reduce(
(acc, x) => lift2(append, x, acc),
T.of([])) // To start us off!
// For example...
// Just [2, 10, 3]
insideOut(Maybe, [ Just(2)
, Just(10)
, Just(3) ])
// Nothing
insideOut(Maybe, [ Just(2)
, Nothing
, Just(3) ])
First of all, we lift an empty list into the applicative context. Then, value by value, we combine the contexts with a function to append
the value to that inner list. Neat, right? We’ll see in a few weeks that this insideOut
can be generalised further to become a super-helpful function called sequenceA
that works on a lot more than just lists!
This is pretty useful for AJAX, too. We can use our
data.task
applicative to create a list of requests - a[Task e a]
- and then combine them into a singleTask
containing the eventual results - aTask e [a]
- usinginsideOut
!
Notice that we only need an Applicative
because the list could be empty. Otherwise, we could just map
over the first with x => [x]
and use that one as the accumulator - we’d only need Apply
! Anyone else having flashbacks to monoids? They’re all very strongly-connected:
-
concat
can combine any non-zero number of values (of the same type) into one. -
ap
can combine any non-zero number of contexts (of the same type) into one. -
If we might need to handle zero values, we will need to use
empty
. -
If we might need to handle zero contexts, we will need to use
of
.
Wizardry. For our next trick, let’s notice that you can turn any Applicative
into a valid Monoid
if the inner type is a Monoid
:
// Note: we need a TypeRep to get empty!
const MyApplicative = T => {
// Whatever your instance is...
// const MyApp = daggy.tagged('MyApp', ['x'])
// Put your map/ap/of here...
// concat :: Semigroup a => MyApp a
// ~> MyApp a
// -> MyApp a
MyApp.prototype.concat =
function (that) {
return lift2((x, y) => x.concat(y),
this, that)
}
// empty :: Monoid a => () -> MyApp a
MyApp.prototype.empty =
() => MyApp.of(T.empty())
return MyApp
}
The above will always be valid. Notice that it doesn’t care about the shape of our Applicative
at all - we can write these implementations using just the interface (map
, ap
, and of
) for any applicative. Of course, Applicative
types can use different implementations for Monoid
. For example, look at Maybe
:
// Usual implementation:
Just([2]).concat(Just([3])) // Just([2, 3])
Just([2]).concat(Nothing) // Just([2])
Nothing.concat(Just([3])) // Just([3])
Nothing.concat(Nothing) // Nothing
Maybe.empty = () => Nothing
// With the above implementation:
Just([2]).concat(Just([3])) // Just([2, 3])
Just([2]).concat(Nothing) // Nothing
Nothing.concat(Just([3])) // Nothing
Nothing.concat(Nothing) // Nothing
Maybe.empty = () => Just(
MyInnerType.empty())
A type might have more than one implementation for any given typeclass (such as Semigroup
); the choice is up to the implementer and the users! As we can see, Maybe
’s usual implementation probably works better. Particularly the empty
bit.
Still, if we have an Applicative
type, we know for sure that have at least one valid Monoid
definition! Magical, right?
All the Apply
types we’ve seen so far have, coincidentally, been Applicative
. We can see it’s pretty easy in many cases to make an of
:
Array.of = x => [x]
Either.of = x => Right(x)
Function.of = x => _ => x
Maybe.of = x => Just(x)
Task.of = x => new Task((_, res) => res(x))
We’re really just constructing the simplest possible value within a type that can hold a value for us. No tricks, nothing fancy. Even Task
, if you remember our Promise
analogy, is about as routine as we could make it. However, let’s talk about pairs:
// Pair :: (l, r) -> Pair l r
const Pair = daggy.tagged('Pair', ['x', 'y'])
// Map over the RIGHT side. The functor
// is `Pair l` and `r` is the inner type.
// map :: Pair l r ~> (r -> s)
// -> Pair l s
Pair.prototype.map = function (f) {
return Pair(this.x, f(this.y))
}
// Apply this to that, retain this' left.
// ap :: Pair l r ~> Pair l (r -> s)
// -> Pair l s
Pair.prototype.ap = function (that) {
return Pair(this.x, that.y(this.y))
}
// But wait...
// of :: r -> Pair l r
Pair.of = function (x) {
return Pair({WHAT GOES HERE}, x)
}
… Ah. Look at the signature for of
- there’s a magical l
value that just appears! We don’t know what type it is, so we don’t know what it can do, which means we can’t find a value to fill this gap. Sure, we can ap
- because we have two l
values to choose from! - but we can’t of
.
How do we solve this? With the same magical pattern that has been going on throughout this post: we require l
to be a Monoid
. If l
is a Monoid
, we know we can call l.empty()
to get a value for that gap!
// TypeRep!
const Pair = T => {
Pair_ = daggy.tagged('Pair', ['x', 'y'])
// And now we're fine! Hooray!
Pair_.of = x => Pair_(T.empty(), x)
return Pair_
}
Array.empty = () => []
// SUCCESS!
const MyPair = Pair(Array)
MyPair.of(2) // Pair([], 2)
So, an
Apply
is anApplicative
withoutof
. AnApplicative
withoutap
also has a name:Pointed
. However, there are no laws attached to it independently, so it’s not particularly useful on its own - just a bit of trivia!
That, good people of The Internet, is all there is to Applicative
. Most of this was covered in the last post, so there are hopefully no great surprises. The important take away is that the relationship between Semigroup
and Monoid
is very similar to that of Apply
and Applicative
. This isn’t the last time we’ll see such a relationship, either! Isn’t it weird how everything’s connected? Baffles me, at least.
Anyway, I hope this has been useful, and, as always, I’m available on twitter to answer any questions you might have. Next time, we’ll be tackling Alt
. Don’t worry: it’s going to be pretty familiar-looking if you’ve been through all the posts in this series. Until then, though, go forth and Apply
!
Thank you so much for reading, and take care ♥