200 likes | 214 Vues
Learn how monads in Haskell provide a more general framework than functors for mapping over data, achieving modularity and compact code. Explore monad examples and their motivation in building functions like lookup and composition.
E N D
PROGRAMMING IN HASKELL Monads and beyond Based on the Haskell wikibook, the book “Learn You a Haskell for Great Good”, (and a few other sources)
Fixed tree code I posted fixed code for our tree data class on the website. Notes: (on board) All examples from last time should run with this one.
Last time: Monads • A monad is defined by 3 things: • A type constructor m • A function return • An operator (>>=) – read as “bind” • Main goal: provide even more general framework than functors for “mapping over things”. • Result and main goal: modularity! Results in more powerful and compact code. • Note that many consider these one of the most powerful (and difficult to understand) features of haskell.
Monads example: Maybe return :: a -> Maybe a return x = Just x (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b m >>= g = case m of Nothing -> Nothing Just x -> g x • Maybe is the monad, and return brings a value into it by wrapping it with Just. • As for (>>=), it takes a m :: Maybe a value and a g :: a -> Maybe b function. If m is Nothing, there is nothing to do and the result is Nothing. Otherwise, in the Just x case, g is applied to x, the underlying value wrapped in Just, to give a Maybe b result. • To sum it all up, if there is an underlying value of type a in m, we apply g to it, which brings the underlying value back into the Maybe monad.
Monads: motivation Continuing this example: Imagine a family database with two functions, which look up the parents of a person: father :: Person -> Maybe Person mother :: Person -> Maybe Person The Maybe is there in case our database is missing some information. We then might want to build other functions, to look up grandparents, etc(next slide)
Monads: motivation Now, let’s find grandparents: maternalGrandfather :: Person -> Maybe Person maternalGrandfather p = case mother p of Nothing -> Nothing Just mom -> father mom But there’s a better way! Maybe is a monad, so can do: maternalGrandfather p = mother p >>= father
Monads: Even more drastic: bothGrandfathers :: Person -> Maybe (Person, Person) bothGrandfathers p = case father p of Nothing -> Nothing Just dad -> case father dad of Nothing -> Nothing Just gf1 -> -- found first grandfather case mother p of Nothing -> Nothing Just mom -> case father mom of Nothing -> Nothing Just gf2 -> -- found second grandfather Just (gf1, gf2) Becomes: bothGrandfathers p = father p >>= (\dad -> father dad >>= (\gf1 -> mother p >>= -- gf1 is only used in the final return (\mom -> father mom >>= (\gf2 -> return (gf1,gf2) ))))
Monads: even better In fact, that grandfather example can be made even better if we use the do notation with braces and semi-colons. This will look more like imperative code that we’re used to! (Like IOs.) Here, father and mother are functions that might fail to produce results, i.e. raise an exception, and when that happens, the whole do-block will fail, i.e. terminate with an exception. bothGrandfathers p = do { dad <- father p; gf1 <- father dad; mom <- mother p; gf2 <- father mom; return (gf1, gf2); }
Monad laws Monads are required to obey 3 laws: m >>= return = m -- right unit return x >>= f = f x -- left unit (m >>= f) >>= g = m >>= (\x -> f x >>= g) -- associativity Why??
Another monad: lists Lists are another commonly used monad! Consider the following code: [(x,y) | x <- [1,2,3] , y <- [1,2,3], x /= y] This is list comprehension, like many we’ve seen so far. Output?
Another monad: lists This code: [(x,y) | x <- [1,2,3] , y <- [1,2,3], x /= y] Is actually equivalent to the following: do x <- [1,2,3] y <- [1,2,3] True <- return (x /= y) return (x,y) Here, each <- generates a set of values which are passed to the rest of the monadic expression. So x <- calls the rest 3 times, and the result (x,y) is returned once for each evaluation.
Applicative type class An applicative functor is something between a “normal” functor and a monad: it allows applying functions within the functor. The primary operation is <*>: GHCi> :t (<*>) (<*>) :: Applicative f => f (a -> b) -> f a -> f b For example: instance Applicative Maybe where pure = Just (Just f) <*> (Just x) = Just (f x) _ <*> _ = Nothing
Applicative type class Why? Makes life easier! Imagine if you wanted to sum Just 2 and Just 3 together. Brute force approach would be to extract values from the wrapper with pattern matching – annoying, since tons of Maybe checks. What we’d like is an operator something like f (a -> b) -> f a -> f b, so we can apply functions in the context of a functor. (Go check previous slide – that’s what <*> is!)
Applicative in action In action, it looks something like this: Prelude> (+) <$> Just 2 <*> Just 3 Just 5 Expressions such as this one are said to be written in applicative style, which is as close as we can get to regular function application while working with a functor. The other operation, pure, just wraps something up in context.
Applicative laws As with all of these, there are rules for this type class. pure id <*> v = v -- Identity pure f <*> pure x = pure (f x) -- Homomorphism u <*> pure y = pure ($ y) <*> u -- Interchange pure (.) <*> u <*> v <*> w = u <*> (v <*> w) -- Composition Again, these just keep things behaving like they should.
Applicative versus monad In fact, some similarities come out: pure :: Applicative f => a -> f a return :: Monad m => a -> m a The only difference is the class constraint (the m in the monad one). Really, pure and return serve the same purpose; that is, bringing values into functors.
Applicative functor: lists We can use <*> over lists, too: Prelude> [(2*),(5*),(9*)] <*> [1,4,7] [2,8,14,5,20,35,9,36,63] The only difference is the class constraint (the m in the monad one). Really, pure and return serve the same purpose; that is, bringing values into functors.
Recap: a sliding scale of power Functor, Applicative, and monad: ignoring pure and return, we get an increasing amount of power: fmap :: Functor f => (a -> b) -> f a -> f b (<*>) :: Applicative f => f (a -> b) -> f a -> f b (>>=) :: Monad m => m a -> (a -> m b) -> m b Rewording a bit: (<$>) :: Functor t => (a -> b) -> (t a -> t b) (<*>) :: Applicative t => t (a -> b) -> (t a -> t b) (=<<) :: Monad t => (a -> t b) -> (t a -> t b)
Recap: a sliding scale of power What does it mean?? (<$>) :: Functor t => (a -> b) -> (t a -> t b) (<*>) :: Applicative t => t (a -> b) -> (t a -> t b) (=<<) :: Monad t => (a -> t b) -> (t a -> t b) Armed with these functions, =<< is the only one which allows us to create contexts (a -> t b) with our inputs. Applicative allows only the combining of contexts we're bringing along. Functor allows no effects to the context at all.
Using these things Even if you never turn your data types into these, you use them constantly in Haskell – everything from lists on up builds upon these operations, and give Haskell its strength. However, making even basic data types in Haskell will be much easier if you give it some thought. There are many examples, from error message passing to finite state machines to complex data structures. (I’ll post a few links on the page for more reading, but most get too long to cover in a lecture!)