210 likes | 371 Views
Functors in Haskell. Adapted from material by Miran Lipovaca. Functors. Functors are a typeclass, just like Ord, Eq, Show, and all the others. This one is designed to hold things that can be mapped over; for example, lists are part of this typeclass. class Functor f where
E N D
Functors in Haskell Adapted from material by Miran Lipovaca
Functors Functors are a typeclass, just like Ord, Eq, Show, and all the others. This one is designed to hold things that can be mapped over; for example, lists are part of this typeclass. classFunctorfwhere fmap::(a->b)->fa->fb Only one typeclass method, called fmap. Basically, says: give me a function that takes a and returns b and a “box” with type a inside of it, and I’ll return a “box” with type b inside of it. 1
Compare fmap to map: fmap::(a->b)->fa->fb map :: (a -> b) -> [a] -> [b] So map is a lot like a functor! Here, map takes a function and a list of type a, and returns a list of type b. In fact, can define map in terms of fmap: instanceFunctor[]where fmap=map 2
Notice what we wrote: instanceFunctor[]where fmap=map We did NOT write “instance Functor [a] where…”, since f has to be a type constructor that takes one type. Here, [a] is already a concrete type, while [] is a type constructor that takes one type and can produce many types, like [Int], [String], [[Int]], etc. 3
Another example: instanceFunctorMaybewhere fmapf(Justx)=Just(fx) fmapfNothing=Nothing Again, we did NOT write “instance Functor (Maybe m) where…”, since functor wants a type constructor. Mentally replace the f’s with Maybe, so fmap acts like (a -> b) -> Maybe a -> Maybe b. If we put (Maybe m), would have (a -> b) -> (Maybe m) a -> (Maybe m) b, which looks wrong. 4
Using it: ghci>fmap(++"HEYGUYSIMINSIDETHE JUST")(Just"Somethingserious.") Just"Somethingserious.HEYGUYSIMINSIDETHEJUST" ghci>fmap(++"HEYGUYSIMINSIDETHE JUST")Nothing Nothing ghci>fmap(*2)(Just200) Just400 ghci>fmap(*2)Nothing Nothing 5
Another example - the tree class. • We’ll define a tree to be either: • an empty tree • an element with a value and two (sub)trees dataTreea=EmptyTree| Nodea(Treea)(Treea) deriving(Show,Read,Eq) This is slightly different than our last definition. Here, trees will be of the form: Node 5 (Node 3 (Node 1 EmptyTree EmptyTree) (Node 4 EmptyTree EmptyTree)) (Node 6 EmptyTree EmptyTree) 6
Some tree functions: singleton::a->Treea singletonx=NodexEmptyTreeEmptyTree treeInsert::(Orda)=>a->Treea->Treea treeInsertxEmptyTree=singletonx treeInsertx(Nodealeftright) |x==a=Nodexleftright |x<a=Nodea(treeInsertxleft)right |x>a=Nodealeft(treeInsertxright) 7
And now to find an element in the tree: treeElem::(Orda)=>a->Treea->Bool treeElemxEmptyTree=False treeElemx(Nodealeftright) |x==a=True |x<a=treeElemxleft |x>a=treeElemxright Note: we deliberately assumed we could compare the elements of the nodes so that we could make this a binary search tree. If this is an “unordered” tree, would need to search both left and right subtrees. 8
An example run: ghci>letnums=[8,6,4,1,7,3,5] ghci>letnumsTree=foldrtreeInsertEmptyTreenums ghci>numsTree Node5(Node3(Node1EmptyTreeEmptyTree)(Node4EmptyTreeEmptyTree))(Node7(Node6EmptyTreeEmptyTree)(Node8EmptyTreeEmptyTree)) 9
Back to functors: If we looked at fmap as though it were only for trees, it would look something like: (a -> b) -> Tree a -> Tree b We can certainly phrase this as a functor, also: instanceFunctorTreewhere fmapfEmptyTree=EmptyTree fmapf(Nodexleftsubrightsub)= Node(fx)(fmapfleftsub) (fmapfrightsub) 10
Using the tree functor: ghci>fmap(*2)EmptyTree EmptyTree ghci>fmap(*4)(foldrtreeInsert EmptyTree[5,7,3,2,1,7]) Node28(Node4EmptyTree(Node8EmptyTree(Node12EmptyTree(Node20EmptyTreeEmptyTree))))EmptyTree 11
Some rules: • The type has to have a kind of * -> *, which means it takes only 1 concrete type as a parameter. Example: ghci>:kMaybe Maybe::*->* 12
If a type takes 2 parameters, like Either: dataEitherab=Lefta |Rightb deriving(Eq,Ord,Read,Show) ghci>Right20 Right20 ghci>:tRight'a' Right'a'::EitheraChar ghci>:tLeftTrue LeftTrue::EitherBoolb
Then the signature :k looks like this: ghci>:kEither Either::*->*->* For this situation, we need to partially apply the type constructor until it takes only 1 input. Remember, Haskell is good at partial evaluations! instance Functor (Either a) where fmap :: (b -> c) -> Either a b -> Either a c
Another functor: the <- in the IO class instanceFunctorIOwhere fmapfaction=do result<-action return(fresult) • Comments: • The result of an IO action must be an IO action, so we’ll use do to glue our 2 things together. • We can use this to make our code more compact, since we won’t need to explicitly “unpack” the IO. • Now (for example) the command: • fmap (++ “!”) getline • Will behave just like getline, but with a ! at the end of the line
Example: program 1 main=doline<-getLine letline'=reverseline putStrLn$"Yousaid"++line'++ "backwards!" With fmap: main=doline<-fmapreversegetLine putStrLn$"Yousaid"++line++ "backwards!"
There are two rules that every functor should obey. Note that these aren’t enforced by Haskell, but they are important! First: If we map the identity function over a functor, then the functor we get back should be the same as the original functor. So: fmap id = id. ghci>fmapid(Just3) Just3 ghci>id(Just3) Just3 ghci>fmapid[1..5] [1,2,3,4,5] ghci>id[1..5] [1,2,3,4,5]
Second rule: composing two functions and then mapping the resulting function over a functor should be the same as first mapping one function over the functor and then mapping the other. So: fmap (f . g) = fmap f . fmap g. Huh? Well, fmap (f . g) (Just x) is (if you go back and look) defined as Just ((f . g) x), which is the same as Just (f (g x)), since we’re just composing functions. And fmap f (fmap g (Just x)) is fmap f (Just (g x)) which is then Just (f (g x)), so we’re ok!
Now why do we care? • If we know that a type obeys both laws, we can make certain assumptions about how it will act. • If a type obeys the functor laws, we know that calling fmap on a value of that type will only map the function over it, nothing more. This leads to code that is more abstract and extensible. • All the Functor examples in Haskell obey these laws by default.