Learning Haskell – Functors
Disclaimer: This content comes out of the fantastic book Effective Haskell written by Rebecca Skinner. I was only talking notes and doing my own little experiments. Original Haskell content by me will follow shortly.
I am learning Haskell and I am having great fun with it. My goal is to be able to apply Denotational Design as described by Conal Elliott to the software I build and learning Haskell gives me a better understanding of the fundamentals, through practice.
I believe that the programming language you work in influences the way you think, so I make a real effort to be fluent in multiple languages with different paradigms.
Functor
For the mathematical definition of functor see Wikipedia.
In programming a functor is a thing that knows how to apply a function to a certain structure preserving that structure. The functor does this via the fmap
function that the typeclass Functor
describes:
fmap :: (a -> b) -> f a -> f b
Read this as: With a function from a to b and a functor of a it returns a functor of b.
There is another function <$
in the functor typeclass that is called replace:
(<$) :: a -> f b -> f a
Imagine the function you want to apply to the functor always returns the same value then the signature becomes clear: With a constant value a and a functor of b it returns a functor of a.
This can always be expressed with fmap
giving it const
as it's function.
Let build our own Functor typeclass in Haskell:
class MyFunctor f where
myFMap :: (a -> b) -> f a -> f b
myReplace :: a -> f b -> f a
myReplace a f = myFMap (const a) f
As mentioned myReplace is already implemented for every instance of out MyFunctor class.
List
To see this in action let's build something simple as our structure like a list:
data MyList a = Empty | MyList a (MyList a) deriving Show
This is your standard recursive list data structure. Let's make it an instance of MyFunctor:
instance MyFunctor MyList where
myFMap _ Empty = Empty
myFMap f (MyList a rest) = MyList (f a) (myFMap f rest)
There you have it. Now let's invent infix operators for myFMap
and myReplace
and try them out:
infixl 4 <.>
(<.>) = myFMap
infixl 4 <.
(<.) = myReplace
(+1) <.> MyList 1 Empty -- Mylist 2 Empty
99 <. Empty -- Empty
99 <. MyList 1 (MyList 2 Empty) -- MyList 99 (MyList 99 Empty)
So you see the Functor instance of our MyList can apply a function to every element preserving the structure of the MyList. The function can change the type of the element but it will always be a list with the same number of elements:
show <.> MyList 1 Empty -- MyList "1" Empty
Maybe
Let's build another structure that can implement the Functor typeclass. I will use German words for the type name and the constructors because, just as List
and Maybe
are already instances of Functor
, and although we will implement MyFunctor
, I think this approach makes it clearer:
data Vielleicht a = Nix | Ein a deriving Show
instance MyFunctor Vielleicht where
myFMap _ Nix = Nix
myFMap f (Ein a) = Ein (f a)
Since MyList
and Vielleicht
are of the same kind: * -> *
that means both taking one parameter to construct a type this was pretty mechanic. Nothing new.
Either
I am using German words again to build an Either
type that has the kind: * -> * -> *
. This means the type constructor will need two parameters to construct a type.
data Entweder a b = Links a | Rechts b deriving show
We can use that to build a save division function:
teile :: (Eq a, Fractional a) => a -> a -> Entweder String a
teile a b
| b == 0 = Links "NaN"
| otherwise = Rechts $ a / b
Now let's think about the Functor instance for Entweder
. I might want to pass round
to the result of teile
but that only needs to be done if the result is an instance of Rechts
. I don't want to round
the String.
So we can implement a sane Functor instance like this:
instance MyFunctor (Entweder a) where
myFMap _ (Links a) = (Links a)
myFMap f (Rechts b) = Rechts $ f a
Let's try it out:
round <.> teile 5 2 -- Rechts 2
Functions
Rebecca shows something else that is cool. Let's create a wrapper for a function so that we can show that even a function can be a functor:
newtype Funktion a b = Funktion
{ run :: (a -> b) }
instance MyFunctor (Funktion a) where
myFMap f (Funktion g) = Funktion (f . g)
run (show <.> Funktion (+1)) 12 -- "13" the type is String, now
Type :i Functor
in ghci and you can see that (->) r
is indeed a Functor
instance. In hindsight this seems obvious. The functor knows how to apply a function f to the result of the function g and gives you a new function f after g – thus the structure is maintained.