Learn You a Haskell for Great Good! - Miran Lipovaca [97]
Chapter 11. Applicative Functors
Haskell’s combination of purity, higher-order functions, parameterized algebraic data types, and type classes makes implementing polymorphism much easier than in other languages. We don’t need to think about types belonging to a big hierarchy. Instead, we consider what the types can act like and then connect them with the appropriate type classes. An Int can act like a lot of things—an equatable thing, an ordered thing, an enumerable thing, and so on.
Type classes are open, which means that we can define our own data type, think about what it can act like, and connect it with the type classes that define its behaviors. We can also introduce a new type class and then make already existing types instances of it. Because of that, and because Haskell’s type system allows us to know a lot about a function just by its type declaration, we can define type classes that define very general and abstract behavior.
We’ve talked about type classes that define operations for seeing if two things are equal and comparing two things by some ordering. Those are very abstract and elegant behaviors, although we don’t think of them as very special, since we’ve been dealing with them for most of our lives. Chapter 7 introduced functors, which are types whose values can be mapped over. That’s an example of a useful and yet still pretty abstract property that type classes can describe. In this chapter, we’ll take a closer look at functors, along with slightly stronger and more useful versions of functors called applicative functors.
Functors Redux
As you learned in Chapter 7, functors are things that can be mapped over, like lists, Maybes, and trees. In Haskell, they’re described by the type class Functor, which has only one type class method: fmap. fmap has a type of fmap :: (a -> b) -> f a -> f b, which says, “Give me a function that takes an a and returns a b and a box with an a (or several of them) inside it, and I’ll give you a box with a b (or several of them) inside it.” It applies the function to the element inside the box.
We can also look at functor values as values with an added context. For instance, Maybe values have the extra context that they might have failed. With lists, the context is that the value can actually be several values at once or none. fmap applies a function to the value while preserving its context.
If we want to make a type constructor an instance of Functor, it must have a kind of * -> *, which means that it takes exactly one concrete type as a type parameter. For example, Maybe can be made an instance because it takes one type parameter to produce a concrete type, like Maybe Int or Maybe String. If a type constructor takes two parameters, like Either, we need to partially apply the type constructor until it takes only one type parameter. So we can’t write instance Functor Either where, but we can write instance Functor (Either a) where. Then if we imagine that fmap is only for Either a, it would have this type declaration:
fmap :: (b -> c) -> Either a b -> Either a c
As you can see, the Either a part is fixed, because Either a takes only one type parameter.
I/O Actions As Functors
You’ve learned how a lot of types (well, type constructors really) are instances of Functor: [], and Maybe, Either a, as well as a Tree type that we created in Chapter 7. You saw how you can map functions over them for great good. Now, let’s take a look at the IO instance.
If some value has a type of, say, IO String, that means it’s an I/O action that will go out into the real world and get some string for us, which it will then yield as a result. We can use <- in do syntax to bind that result to a name. In Chapter 8, we talked about how I/O actions are like boxes with little feet that go out and fetch some value from the outside world for us. We can inspect what they fetched, but after inspecting, we need to wrap the value back in IO. Considering this box with feet analogy, you can see how IO acts like a functor.
Let’s see