Right now I'm learning Haskell, a functional programming language. In my last few Haskell articles I've discussed basic aspects of the language. Now I've begun digging into advanced functional programming concepts. This article discusses functors, a generic way to map functions over objects.
A common pattern in programming is looping through a collection of values and applying a transformation to each. For example, a programmer might loop through a list of integers and increment each value. The imperative approach to this transformation sets up a for loop and iterates over each list index. The functional approach uses the map higher-order function. map accepts two arguments - a collection to iterate over and a function. The function is applied to each item in the collection.
Haskell has a map function defined in the GHC Prelude module1. It accepts a list and a function as arguments.
With the map function, list items can be transformed. A basic example is incrementing each item in the list.
Haskell's map function is useful, however its restricted to just a list data structure. The concept of applying a function across all items in a data structure can be generalized even more than the map function. This is where functors come into play.
In Haskell (and functional programming in general), a functor provides the ability to map a function across all the items in a data structure or object2. Functors in Haskell are defined by a Functor type class. Types that are instances of Functor implement a function called fmap, which provides mapping functionality.
The Functor type class definition is simple:
fmap is a curried function that takes in another function [(a -> b)] and a value whose type is an instance of Functor [f a] as arguments. fmap passes f a to the function (a -> b), resulting in b.
The most basic functor instance is for the list type3.
For lists, fmap is implemented the same as map. Therefore, it behaves as expected.
The great thing about functors is any parameterized type in Haskell can be made an instance of Functor. For example, the Maybe type which defines a potentially non-existant value can become a functor.
Custom types can also become functors. For example, my custom Distance type is easily made an instance of Functor.
Distance is a valid functor instance because its data definition is parameterized and it has a type constructor. You can think of Distance as a container type, since it holds a value such as a floating point number. Lists and the Maybe type have similar structures. Types without a parameterized data definition are not valid functors because they don't contain another type4.
Functors are useful for mapping functions across all items contained in an object. They become even more useful in regards to Applicatives and Monads, which I will discuss in upcoming articles.
For now, one last useful functor implementation is a generic function for all instances of Functor to utilize. With the help of a Functor class constant, a function is easily created to increment the items in any Functor instance5.
Any type that is an instance of Functor can utilize the inc function! This is a really powerful design pattern for function reuse.
Working with a functional programming language forces the brain to think about common problems differently. While initially difficult to reason about, functors simply provide a mechanism for mapping a function across different items. Functors also promote the creation of generic code which works for many different types. In my next article I'll build on this knowledge and explore applicatives. All the code from this article is available on GitHub.