The Easiest Haskell Idiom
Once you master the basics of Haskell, the next important step is to understand the patterns that make it easier to write good, idiomatic Haskell code. The next few posts will focus on some of the most important patterns to learn. The simplest of these is functors.
A Simple Example
Here’s a simple example to start us on our way. This code converts an input string like “John Doe 24” into a tuple. We want to consider all inputs though, so the resulting type is a Maybe
.
tupleFromInputString :: String -> Maybe (String, String, Int)
tupleFromInputString input = if length stringComponents /= 3
then Nothing
else Just (stringComponents !! 0, stringComponents !! 1, age)
where
stringComponents = words input
age = (read (stringComponents !! 2) :: Int)
This simple function simply takes a string and converts it into parameters for first name, last name, and age. Suppose we have another part of our program using a data type to represent a person instead of a tuple. We might write a conversion function between these two different types. We want to account for the possibility of failure. So we’ll have another function handling that case.
data Person = Person {
firstName :: String,
lastName :: String,
age :: Int
}
personFromTuple :: (String, String, Int) -> Person
personFromTuple (fName, lName, age) = Person fName lName age
convertTuple :: Maybe (String, String, Int) -> Maybe Person
convertTuple Nothing = Nothing
convertTuple (Just t) = Just (personFromTuple t)
A Change of Format
But imagine our original program changes to read in a whole list of names:
listFromInputString :: String -> [(String, String, Int)]
listFromInputString contents = mapMaybe tupleFromInputString (lines contents)
tupleFromInputString :: String -> Maybe (String, String, Int)
...
Now if we passed this result to the code using Person
, we would have to change the type of the convertTuple
function. It would have a parallel structure though. Maybe
and List
can both act as containers for other values. Sometimes, we don’t care how values are wrapped. We just want to transform whatever underlying value exists, and then return the new value in the same wrapper.
Introduction to Functors
With this idea in mind, we can start understanding functors. First and foremost, Functor
is a typeclass in Haskell. In order for a data type to be an instance of the Functor
typeclass, it must implement a single function: fmap
.
fmap :: (a -> b) -> f a -> f b
The fmap function takes two inputs. First, it demands a function between two data types. The second parameter is some container of the first type. The output then is a container of the second type. Now let’s look at a few different Functor
instances for some familiar types. For lists, fmap
is simply defined as the basic map function:
instance Functor [] where
fmap = map
In fact, fmap is a generalization of mapping. For example, the Map data type is also a functor. It uses its own map
function for fmap
. Functors simply take this idea of transforming all underlying values and apply it to other types. With this in mind, let’s observe how Maybe
is a functor:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
This looks a lot like our original convertTuple
function! If we have no value in the first place, then the result is Nothing
. If we do have a value, simply apply the function to the value, and rewrap it in Just
. The Either
data type can be seen as a Maybe
type with more information about how it failed. It has similar behavior:
instance Functor (Either a) where
fmap _ (Left x) = Left x
fmap f (Right y) = Right (f y)
Note the first type parameter of this instance is fixed. Only the second parameter of an Either
value is changed by fmap
. Based on these examples, we can see how to rewrite convertTuple
to be more generic:
convertTuple :: Functor f => f (String, String, Int) -> f Person
convertTuple = fmap personFromTuple
Making Our Own Functors
We can also take our own data type and define an instance of Functor
. Suppose we have the following data type representing a directory of local government officials. It is parameterized by the type a
. This means we allow different directories using different representations of a person:
data GovDirectory a = GovDirectory {
mayor :: a,
interimMayor :: Maybe a,
cabinet :: Map String a,
councilMembers :: [a]
}
One part of our application might represent people with tuples. Its type would be GovDirectory (String, String, Int)
. However, another part could use the type GovDirectory Person
. We can define the following Functor
instance for GovDirectory
by defining fmap
. Since our underlying types are mostly functors themselves, this mostly involves calling fmap
on the individual fields!
instance Functor GovDirectory where
fmap f oldDirectory = GovDirectory {
mayor = f (mayor oldDirectory),
interimMayor = f <$> interimMayor oldDirectory,
cabinet = f <$> cabinet oldDirectory,
councilMembers = f <$> councilMembers oldDirectory
}
Note <$>
is simply a synonym for fmap
. Now we have our own functor instance, sp transforming the underlying data type of our directory class is easy! We can just use:
convertTuple <$> oldDirectory
Summary
- Functors, in general, wrap some kind of data
- In Haskell,
Functor
is a typeclass, with a single functionfmap
- The
fmap
function allows us to transform the underlying data without caring how the data is contained. - This allows us to write much more flexible code in certain circumstances.
Stay tuned for next week, when we’ll discuss applicative functors! If you’re starting to get a grasp for Haskell and want to try new skills, be sure to check out our free workbook on Recursion, which comes with 10 practice problems!