12

I was playing around with composable failures and managed to write a function with the signature

getPerson :: IO (Maybe Person)

where a Person is:

data Person = Person String Int deriving Show

It works and I've written it in the do-notation as follows:

import Control.Applicative

getPerson = do
    name <- getLine -- step 1
    age  <- getInt  -- step 2
    return $ Just Person <*> Just name <*> age 

where

getInt :: IO (Maybe Int)
getInt = do
    n <- fmap reads getLine :: IO [(Int,String)]
    case n of
        ((x,""):[])   -> return (Just x)
        _ -> return Nothing

I wrote this function with the intent of creating composable possible failures. Although I've little experience with monads other than Maybe and IO this seems like if I had a more complicated data type with many more fields, chaining computations wouldn't be complicated.

My question is how would I rewrite this without do-notation? Since I can't bind values to names like name or age I'm not really sure where to start.

The reason for asking is simply to improve my understanding of (>>=) and (<*>) and composing failures and successes (not to riddle my code with illegible one-liners).

Edit: I think I should clarify, "how should I rewrite getPerson without do-notation", I don't care about the getInt function half as much.

Dave
  • 257
  • 2
  • 7
  • 2
    See also: [Haskell 2010 Report > Expressions # Do Expressions](http://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-470003.14) – Dan Burton Aug 29 '11 at 13:32
  • 1
    Just to nitpick, note that `getPerson` isn't a function, since it has no `->` in its type signature; if you want a more precise name than "value", I'd go with "IO action". See ["Everything is a function" in Haskell?](http://conal.net/blog/posts/everything-is-a-function-in-haskell) for more on this. – Antal Spector-Zabusky Aug 29 '11 at 18:01

3 Answers3

23

Do-notation desugars to (>>=) syntax in this manner:

getPerson = do
    name <- getLine -- step 1
    age  <- getInt  -- step 2
    return $ Just Person <*> Just name <*> age

getPerson2 =
  getLine >>=
   ( \name -> getInt >>=
   ( \age  -> return $ Just Person <*> Just name <*> age ))

each line in do-notation, after the first, is translated into a lambda which is then bound to the previous line. It's a completely mechanical process to bind values to names. I don't see how using do-notation or not would affect composability at all; it's strictly a matter of syntax.

Your other function is similar:

getInt :: IO (Maybe Int)
getInt = do
    n <- fmap reads getLine :: IO [(Int,String)]
    case n of
        ((x,""):[])   -> return (Just x)
        _ -> return Nothing

getInt2 :: IO (Maybe Int)
getInt2 =
    (fmap reads getLine :: IO [(Int,String)]) >>=
     \n -> case n of
        ((x,""):[])   -> return (Just x)
        _             -> return Nothing

A few pointers for the direction you seem to be headed:

When using Control.Applicative, it's often useful to use <$> to lift pure functions into the monad. There's a good opportunity for this in the last line:

Just Person <*> Just name <*> age

becomes

Person <$> Just name <*> age

Also, you should look into monad transformers. The mtl package is most widespread because it comes with the Haskell Platform, but there are other options. Monad transformers allow you to create a new monad with combined behavior of the underlying monads. In this case, you're using functions with the type IO (Maybe a). The mtl (actually a base library, transformers) defines

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

This is the same as the type you're using, with the m variable instantiated at IO. This means you can write:

getPerson3 :: MaybeT IO Person
getPerson3 = Person <$> lift getLine <*> getInt3

getInt3 :: MaybeT IO Int
getInt3 = MaybeT $ do
    n <- fmap reads getLine :: IO [(Int,String)]
    case n of
        ((x,""):[])   -> return (Just x)
        _             -> return Nothing

getInt3 is exactly the same except for the MaybeT constructor. Basically, any time you have an m (Maybe a) you can wrap it in MaybeT to create a MaybeT m a. This gains simpler composability, as you can see by the new definition of getPerson3. That function doesn't worry about failure at all because it's all handled by the MaybeT plumbing. The one remaining piece is getLine, which is just an IO String. This is lifted into the MaybeT monad by the function lift.

Edit newacct's comment suggests that I should provide a pattern matching example as well; it's basically the same with one important exception. Consider this example (the list monad is the monad we're interested in, Maybe is just there for pattern matching):

f :: Num b => [Maybe b] -> [b]
f x = do
  Just n <- x
  [n+1]

-- first attempt at desugaring f
g :: Num b => [Maybe b] -> [b]
g x = x >>= \(Just n) -> [n+1]

Here g does exactly the same thing as f, but what if the pattern match fails?

Prelude> f [Nothing]
[]

Prelude> g [Nothing]
*** Exception: <interactive>:1:17-34: Non-exhaustive patterns in lambda

What's going on? This particular case is the reason for one of the biggest warts (IMO) in Haskell, the Monad class's fail method. In do-notation, when a pattern match fails fail is called. An actual translation would be closer to:

g' :: Num b => [Maybe b] -> [b]
g' x = x >>= \x' -> case x' of
                      Just n -> [n+1]
                      _      -> fail "pattern match exception"

now we have

Prelude> g' [Nothing]
[]

fails usefulness depends on the monad. For lists, it's incredibly useful, basically making pattern matching work in list comprehensions. It's also very good in the Maybe monad, since a pattern match error would lead to a failed computation, which is exactly when Maybe should be Nothing. For IO, perhaps not so much, as it simply throws a user error exception via error.

That's the full story.

John L
  • 27,937
  • 4
  • 73
  • 88
  • I understood that do-notation was syntax sugar for (>>=) but I'd never seen exactly how. That makes sense and looks like ordinary lambda calculus. As for the rest of your answer thanks again. 'Person <$> Just name <*> age' is clearly neater and I'll likely 'lift' pure functions like that in the future, whatever lift means. Which brings me to the third part of your answer where you introduce mtl and monad transformers. I've never read about these and don't know anything about them as I always assumed they were further away from what I was experimenting with. It seems I'll start on them next. – Dave Aug 29 '11 at 12:13
  • Ch. 18 of Real World Haskell (http://book.realworldhaskell.org/read/monad-transformers.html) introduces monad transformers. LYAH unfortunately doesn't seem to cover them. Lifting, in general, means that you take something of some regular type and put it into a fancier context. So using `<$>` lifts a pure function into an applicative, and the `lift` function of a monad transformer takes a computation in the underlying monad and lifts it into the fancy combined monad. – John L Aug 29 '11 at 13:40
  • 1
    when the thing on the left side of `<-` is a pattern that is not complete, then it gets more complicated – newacct Aug 29 '11 at 22:45
  • 2
    A very minor comment: I would write `Person <$> Just name <*> age` as `Person name <$> age` -- it more clearly delineates where there are effects (in `age`) and where not (in `Person name`). – Daniel Wagner Aug 30 '11 at 00:14
  • @newacct: good point; I've edited to include this information. – John L Aug 30 '11 at 16:32
4

do-blocks of the form var <- e1; e2 desugar to expressions using >>= as follows e1 >>= \var -> e2. So your getPerson code becomes:

getPerson =
    getLine >>= \name ->
    getInt  >>= \age ->
    return $ Just Person <*> Just name <*> age

As you see this is not very different from the code using do.

sepp2k
  • 363,768
  • 54
  • 674
  • 675
  • Thanks, I was aware do-notation was sugared (>>=) but not sure exactly how. I was flailing away at ghci trying to get the expected ('my' expected) outcome without success. – Dave Aug 29 '11 at 12:15
  • 1
    This looks much more pleasing then then when people use nested parens. – Davorak Aug 30 '11 at 14:39
1

Actually, according to this explaination, the exact translation of your code is

getPerson = 
    let f1 name = 
                  let f2 age = return $ Just Person <*> Just name <*> age
                      f2 _ = fail "Invalid age"
                  in getInt >>= f2
        f1 _ = fail "Invalid name"
    in getLine >>= f1

getInt = 
    let f1 n = case n of
               ((x,""):[])   -> return (Just x)
               _ -> return Nothing
        f1 _ = fail "Invalid n"
    in (fmap reads getLine :: IO [(Int,String)]) >>= f1

And the pattern match example

f x = do
  Just n <- x
  [n+1]

translated to

f x =
  let f1 Just n = [n+1]
      f1 _ = fail "Not Just n"
  in x >>= f1

Obviously, this translated result is less readable than the lambda version, but it works with or without pattern matching.

Earth Engine
  • 10,048
  • 5
  • 48
  • 78