Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write without Do notation

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.

like image 227
Dave Avatar asked Aug 29 '11 11:08

Dave


People also ask

What does >> mean in Haskell?

Essentially, a >> b can be read like "do a then do b , and return the result of b ". It's similar to the more common bind operator >>= .

Do statements in Haskell?

As a syntactical convenience, do notation does not add anything essential, but it is often preferable for clarity and style. However, do is not needed for a single action, at all. The Haskell "Hello world" is simply: main = putStrLn "Hello world!"

What does a period do in Haskell?

In other words, the period is used to take the result from the function on the right, feed it as a parameter to the function on the left, and return a new function that represents this computation."


2 Answers

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.

like image 141
John L Avatar answered Sep 24 '22 04:09

John L


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.

like image 45
sepp2k Avatar answered Sep 24 '22 04:09

sepp2k