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:
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:
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:becomes
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) definesThis is the same as the type you’re using, with the
mvariable instantiated atIO. This means you can write:getInt3is exactly the same except for theMaybeTconstructor. Basically, any time you have anm (Maybe a)you can wrap it inMaybeTto create aMaybeT m a. This gains simpler composability, as you can see by the new definition ofgetPerson3. That function doesn’t worry about failure at all because it’s all handled by the MaybeT plumbing. The one remaining piece isgetLine, which is just anIO String. This is lifted into the MaybeT monad by the functionlift.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,
Maybeis just there for pattern matching):Here
gdoes exactly the same thing asf, but what if the pattern match fails?What’s going on? This particular case is the reason for one of the biggest warts (IMO) in Haskell, the
Monadclass’sfailmethod. In do-notation, when a pattern match failsfailis called. An actual translation would be closer to:now we have
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 theMaybemonad, since a pattern match error would lead to a failed computation, which is exactly whenMaybeshould beNothing. ForIO, perhaps not so much, as it simply throws a user error exception viaerror.That’s the full story.