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.
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]
[]
fail
s 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.
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
.
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.