Functors Done Quick!
Suppose we're writing some code to deal with bank accounts. Most of our code will refer to these using a proper data type. But less refined parts of our code might use a tuple with the same information instead. We would want a conversion function to go between them. Here's a simple example:
data BankAccount = BankAccount
{ bankName :: String
, ownerName :: String
, accountBalance :: Double
}
convertAccount :: (String, String, Double) -> BankAccount
convertAccout (bank, owner, balance) = BankAccount bank owner balance
Naturally, we'll want a convenience function for performing this operation on a list of items. We'll can use map
for lists.
convertAccounts :: [(String, String, Double)] -> [BankAccount]
convertAccounts = map convertAccount
But Haskell has a plethora of different data structures. We can store our data in a Set
, or a Vector
, for a couple examples. What if different parts of our code store the data differently? They would need their own conversion functions, since the list version of map
doesn't work on a Set
or Vector
. Can we make this code more generic?
Functors
If you read the blog post a couple weeks ago, you'll remember the idea of typeclasses. This is how we can make our code generic! We want to generalize the behavior of running a transformation over a data structure. We can make a typeclass to encapsulate this behavior. Luckily, Haskell already has such a typeclass, called Functor
. It has a single function, fmap
. Here is how it is defined:
class Functor f where
fmap :: (a -> b) -> f a -> f b
If that type signature looks familiar, that's because it's almost identical to the map
function over lists. And in fact, the List
type uses map
as it's implementation for fmap
:
map :: (a -> b) -> [a] -> [b]
instance Functor [] where
fmap = map
Other Functor Instances
Now, Set
and Vector
do have map
functions. But to make our code generic, we have to define functor instances as a go-between:
instance Functor Vector where
fmap = Data.Vector.map
instance Functor Set where
fmap = Data.Set.map
With all this in mind, we can now rewrite convertAccounts
generically.
convertAccounts :: (Functor f) => f (String, String, Double) -> f BankAccount
convertAccounts = fmap convertAccount
Now anything can use convertAccounts
no matter how it structures the data, as long as it uses a functor! Let's looks at some of the other functors out there!
While it might not seem to fit in the same category as lists, vectors and sets, Maybe
is also a functor! Here's its implementation:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
Another example of a functor is Either
. This one is a little confusing since Either
has two type parameters. But really, we have to fix the first parameter. Then the conversion function is only applied to the second. This means that, like with the Nothing
case above, when we have Left
, we return the original value:
instance Functor (Either a) where
fmap _ (Left a) = Left a
fmap f (Right x) = Right (f x)
Conceptualizing Functors
So concretely, Functor
is a typeclass in Haskell. But how can we think of it conceptually? This is actually pretty simple. A functor is nothing more than a generic container or box. We don't know how many elements it contains. We don't know what the structure of those elements is. But if we have a way to transform those elements, we can apply that function over all of them. The result will be a new container with the same structure, but new elements. As far as abstractions go, this is probably the cleanest one we'll get, so enjoy it!
Conclusion
Functor is an example of typeclass that we can use to get general behavior. In this case, the behavior is transforming a group of objects in a container while maintaining the container's structure. We saw how this typeclass allowed us to re-use a function over many different types. Functors are the simplest in a series of important typeclasses. Applicative functors would come next, and then monads. Monads are vital to Haskell. So understanding functors is an important first step towards learning more complex Haskell.
But you can't learn about data structures until you know the basics! If you've never written any Haskell before, download out Getting Started Checklist! If you're comfortable with the basics and want more of a challenge, take a look at our Recursion Workbook!