Unit 5. Working with type in a context

In this unit, you’ll take a look at three of Haskell’s most powerful and often most confusing type classes: Functor, Applicative, and Monad. These type classes have funny names but a relatively straightforward purpose. Each one builds on the other to allow you to work in contexts such as IO. In unit 4, you made heavy use of the Monad type class to work in IO. In this unit, you’ll get a much deeper understanding of how that works. To get a better feel for what these abstract type classes are doing, you’ll explore types as though they were shapes.

One way to understand functions is as a means of transforming one type into another. Let’s visualize two types as two shapes, a circle and a square, as shown in figure 1.

Figure 1. A circle and square visually representing two types

These shapes can represent any two types, Int and Double, String and Text, Name and FirstName, and so forth. When you want to transform a circle into a square, you use a function. You can visualize a function as a connector between two shapes, as shown in figure 2.

Figure 2. A function can transform a circle to a square.

This connector can represent any function from one type to another. This shape could represent (Int -> Double), (String -> Text), (Name -> FirstName), and so forth. When you want to apply a transformation, you can visualize placing your connector between the initial shape (in this case, a circle) and the desired shape (a square); see figure 3.

Figure 3. Visualizing a function as a way of connecting one shape to another

As long as each shape matches correctly, you can achieve your desired transformation.

In this unit, you’ll look at working with types in context. The two best examples of types in context that you’ve seen are Maybe types and IO types. Maybe types represent a context in which a value might be missing, and IO types represent a context in which the value has interacted with I/O. Keeping with our visual language, you can imagine types in a context as shown in figure 4.

Figure 4. The shape around the shape represents a context, such as Maybe or IO.

These shapes can represent types such as IO Int and IO Double, Maybe String and Maybe Text, or Maybe Name and Maybe FirstName. Because these types are in a context, you can’t simply use your old connector to make the transformation. So far in this book, you’ve relied on using functions that have both their input and output in a context as well. To perform the transformation of your types in a context, you need a connector that looks like figure 5.

Figure 5. A function that connects two types in a context

This connector represents functions with type signatures such as (Maybe Int -> Maybe Double), (IO String -> IO Text), and (IO Name -> IO FirstName). With this connector, you can easily transform types in a context, as shown in figure 6.

Figure 6. As long as your connector matches, you can make the transformation you want.

This may seem like a perfect solution, but there’s a problem. Let’s look at a function halve, which is of the type Int -> Double, and as expected halves the Int argument.

Listing 1. A halve function from Int -> Double
halve :: Int -> Double
halve n = fromIntegral n / 2.0

This is a straightforward function, but suppose you want to halve a Maybe Int. Given the tools you have, you have to write a wrapper for this that works with Maybe types.

Listing 2. halveMaybe wraps halve function to work with Maybe types
halveMaybe :: Maybe Int -> Maybe Double
halveMaybe (Just n) = Just (halve n)
halveMaybe Nothing = Nothing

For this one example, it’s not a big deal to write a simple wrapper. But consider the wide range of existing functions from a -> b. To use any of these with Maybe types would require nearly identical wrappers. Even worse is that you have no way of writing these wrappers for IO types!

This is where Functor, Applicative, and Monad come in. You can think of these type classes as adapters that allow you to work with different connectors so long as the underlying types (circle and square) are the same. In the halve example, you worried about transforming your basic Int-to-Double adapter to work with types in context. This is the job of the Functor type class, illustrated in figure 7.

Figure 7. The Functor type class solves this mismatch between types in a context and a connector.

But you can have three other types of mismatches. The Applicative type class solves two of these. The first occurs when the first part of your connector is in a context, but not its result, as shown in figure 8.

Figure 8. This is one of the mismatches that Applicative solves.

The other problem occurs when an entire function is in a context. For example, a function of the type Maybe (Int -> Double) means you have a function that might itself be missing. This may sound strange, but it can easily happen when using partial application with Maybe or IO types. Figure 9 illustrates this interesting case.

Figure 9. Sometimes the connector itself is trapped in a context; Applicative solves this problem as well.

There’s only one possible mismatch between a function and types in a context left. This occurs when the argument to a function isn’t in a context, but the result is. This is more common than you may think. Both Map.lookup and putStrLn have type signatures like this. This problem is solved by the Monad type class, illustrated in figure 10.

Figure 10. The Monad type class provides an adapter for this final possible mismatch.

When you combine all three of these type classes, there’s no function that you can’t use in a context such as Maybe or IO, so long as the underlying types match. This is a big deal because it means that you can perform any computation you’d like in a context and have the tools to reuse large amounts of existing code between different contexts.