Monad Wars – 2.5: some comments and corrections

One of the advantages of demonstrating your ignorance in public is
that you may receive useful corrections… thanks to everyone who
replied on these recent posts, I found the comments very instructive,
and thought it was worth writing up as a new post.

Strict records

ddarius got in touch to mention that I might want to use “strict fields”. This might be an issue if I'm incrementing, say, turn,
but not actually using the value. I'd end up building up a “thunk” (an
unevaluated expression) like 1+1+1+1+1+1+1, which will get evaluated
later (and if too much later, it could cause some problems like stack
overflow). Actually, I don't think this will happen in this particular
case (I'll be printing the turn count every time) but it's not hard to
implement (just need to put a “!” before the strict fields)

 > data GameState = GameState {
> turn :: !Integer,
> score :: Integer,
> location :: Location
> } deriving Show

Also, as nominolo suggested, in conjunction with -funbox-strict-fields, it can open up some possible optimizations.

Not Just Maybe

Now this is an interesting one. I was whining in my last post about Data.Map.lookup

but which monad is it in, and more to the point, why?

As you might imagine, I really wasn't getting it… and the code I wrote around it rather reflects that…

Vincenz, Rich Neswold, and “rm” all pointed out in rapid succession
that the function I'd created for parseMap was completely redundant.
Here, for comparison, is my first version.

 > parseMap m s | M.member s m = do v <- M.lookup s m
> Just v
> | otherwise = Nothing

I wrote this because, from the ghci command line, it looked like M.lookup threw an error if it couldn't find the key. The suggestion, which is rather briefer is as follows:

 > parseMap = flip M.lookup

The flip is only there because parseMap and Data.Map.lookup take their arguments in opposite orders. Otherwise lookup and parseMap are identical!

But how does this return a “Just” or a “Nothing” appropriately? Apparently, on success, it returns a Monad of the appropriate type by default. If on the other hand it doesn't work, it will fail.

The IO monad maps a fail to an error (which is why I saw the exception I mentioned in the post!) But Maybe will map it to Nothing.

So from the ghci command line, we can create a small test Data.Map and run some lookups against it “in” various monads.

 Prelude Data.Map> let m = Data.Map.fromList ("one", 1)
 -- success
Prelude Data.Map> Data.Map.lookup "one" m :: Maybe String
Just "uno"
 Prelude Data.Map> Data.Map.lookup "one" m  :: [String]
["uno"]
 Prelude Data.Map> Data.Map.lookup "one" m  :: IO String
"uno"
 -- fail
Prelude Data.Map> Data.Map.lookup "two" m :: Maybe String
Nothing
 Prelude Data.Map> Data.Map.lookup "two" m  :: [String]
[]
 Prelude Data.Map> Data.Map.lookup "two" m  :: IO String
*** Exception: user error (Data.Map.lookup: Key not found)

ddarius gave a name to this technique, “Not Just Maybe”. That is, if
you were going to write a function that returns Maybe “Just 1“ and
Maybe “Nothing”, then you might as well just write it as a generic
monad. This will then be usable within Maybe, as planned, but also in
IO and List too.

This sparked an interesting discussion about “Common Idioms” in
Haskell. Apparently the pages that used to exist on this topic haven't
yet been migrated to the new wiki. But there are some notes, for
example on this snapshot of the NotJustMaybe page.

ddarius also suggested I could rewrite parseInt similarly

 > import Control.Monad
>
> parseInt :: MonadPlus m => String -> m Int
> parseInt s | all isDigit s = return $ read s
> | otherwise = mzero

Using MonadPlus, 1) requires the Control.Monad import. 2) seems to
require the type signature. 3) it looks like IO doesn't have an mzero,
so you can't now type

 *Main> parseInt "4" 
<interactive>:1:0:
Ambiguous type variable `m' in the constraint:
`MonadPlus m'

at the command line and have it Do The Right Thing. I'd read that fail is considered bad style (for some reason), but it seems to be rather more convenient on these 3 counts at least:

 > -- type will be inferred if omitted
> parseInt :: Monad m => String -> m Int
> parseInt s | all isDigit s = return $ read s
> | otherwise = fail "not an int"