Functional Programming

Download Report

Transcript Functional Programming

Functors
in Haskell
Adapted from material by Miran Lipovaca
0
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
fmap :: (a -> b) -> f a -> f b
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) -> f a -> f b
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:
instance Functor [] where
fmap = map
2
Notice what we wrote:
instance Functor [] 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:
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = 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 (++ " HEY GUYS IM INSIDE THE
JUST") (Just "Something serious.")
Just "Something serious. HEY GUYS IM INSIDE TH
E JUST"
ghci> fmap (++ " HEY GUYS IM INSIDE THE
JUST") Nothing
Nothing
ghci> fmap (*2) (Just 200)
Just 400
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
data Tree a = EmptyTree |
Node a (Tree a) (Tree a)
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 -> Tree a
singleton x = Node x EmptyTree EmptyTree
treeInsert :: (Ord a) => a -> Tree a -> Tree a
treeInsert x EmptyTree = singleton x
treeInsert x (Node a left right)
| x == a = Node x left right
| x < a = Node a (treeInsert x left) right
| x > a = Node a left (treeInsert x right)
7
And now to find an element in the tree:
treeElem :: (Ord a) => a -> Tree a -> Bool
treeElem x EmptyTree = False
treeElem x (Node a left right)
| x == a = True
| x < a = treeElem x left
| x > a = treeElem x right
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> let nums = [8,6,4,1,7,3,5]
ghci> let numsTree = foldr treeInsert EmptyTre
e nums
ghci> numsTree
Node 5 (Node 3 (Node 1 EmptyTree EmptyTree) (N
ode 4 EmptyTree EmptyTree)) (Node 7 (Node 6 Em
ptyTree EmptyTree) (Node 8 EmptyTree EmptyTre
e))
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:
instance Functor Tree where
fmap f EmptyTree = EmptyTree
fmap f (Node x leftsub rightsub) =
Node (f x) (fmap f leftsub)
(fmap f rightsub)
10
Using the tree functor:
ghci> fmap (*2) EmptyTree
EmptyTree
ghci> fmap (*4) (foldr treeInsert
EmptyTree [5,7,3,2,1,7])
Node 28 (Node 4 EmptyTree (Node 8 EmptyTree (N
ode 12 EmptyTree (Node 20 EmptyTree EmptyTree
)))) 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> :k Maybe
Maybe :: * -> *
12
If a type takes 2 parameters, like Either:
data Either a b = Left a
| Right b
deriving (Eq, Ord, Read, Show)
ghci> Right 20
Right 20
ghci> :t Right 'a'
Right 'a' :: Either a Char
ghci> :t Left True
Left True :: Either Bool b
Then the signature :k looks like this:
ghci> :k Either
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
instance Functor IO where
fmap f action = do
result <- action
return (f result)
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 = do line <- getLine
let line' = reverse line
putStrLn $ "You said " ++ line' ++
" backwards!"
With fmap:
main = do line <- fmap reverse getLine
putStrLn $ "You said " ++ 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> fmap id (Just 3)
Just 3
ghci> id (Just 3)
Just 3
ghci> fmap id [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.