How to write without Do notation

2019-04-21 03:15发布

问题:

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.

回答1:

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.



回答2:

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.



回答3:

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.