While playing around with querying ElasticSearch I bumped into something that I hadn’t really understood explicitly before about monads, nesting, and IO. Rather than blather on, I’m going to share a “literate” ghci session that demonstrates the point. Main editing change I made was to remove duplication in the output from querying the type
:t in ghci.
Importing the HTTP client library, a simple one based on IO. Also binding the query I want to perform, “find all documents with the field port equal to 3000, return 1 document”.
But that doesn’t print anything. It type-checks too! Let’s break it down.
So we’ve got an IO action returning a String. IO happens to implement the monad typeclass, so we should be able to
>>= (bind) functions against it.
For our purposes,
liftM mean the same thing. Lifting a function to work on data inside the IO monad.
So far, everything about how we expect this stuff lines up with reality, except for that one case where the body of the response doesn’t print when we lift putStrLn to operate on the IO String returned by getResponseBody.
On a hunch, I broke out putStrLn into a separate bind (
>>=) call, allowing me to eliminate the lifting (
The above is the right answer!
This is how you should write your code in real Haskell. Eliminating the unnecessary lifting was what I should’ve done to begin with.
All of the code that follows this point is just from my wanting to explore the matter further and to try to get the lifted version to work properly for its own sake.
Then on another hunch:
So at this point I felt a little dumb, if you do too, it’s okay.
>>= id is just
join. As demonstrated with lists:
Note that in this case,
m (m a) is
[[Int]] in our list example.
[[1, 2, 3], [4, 5, 6]] is
If this seems like a garden path, just bear with me, I’m tying it back to the original misunderstanding earlier.
Let us query some types in our REPL again:
Our mistake was to
putStrLn instead of just binding it separately of
getResponseBody. The nested and unevaluated
IO (IO ()) action led to the HTTP response not getting printed.
With the above type signatures in mind:
Now for the grand reveal.
IO (IO ()) is sometimes a sign of a mistake in Haskell code. There are times it’s needed, such as is demonstrated in my next blog post. We don’t force evaluation of the nested function(s) until we use
join to flatten the IO (IO ()) into IO ().
But the code we really want to write is:
Often you just want to bind.
Reminders/type cheat sheet:
λ> :t (>>=) (>>=) :: Monad m => m a -> (a -> m b) -> m b λ> :t (join) (join) :: Monad m => m (m a) -> m a λ> :t (fmap) (fmap) :: Functor f => (a -> b) -> f a -> f b λ> :t (liftM) (liftM) :: Monad m => (a1 -> r) -> m a1 -> m r --- note how this looks a lot like >>= (bind) ? λ> :t (join .) . fmap :: (Monad m, Functor m) => (a1 -> m a) -> m a1 -> m a --- or simply flipped around: λ> :t flip ((join .) . fmap) :: (Monad m, Functor m) => m a1 -> (a1 -> m a) -> m a λ> concat [[1, 2, 3], [4, 5, 6]] == join [[1, 2, 3], [4, 5, 6]] True
Reminder: The Monad typeclass in Haskell is only return, bind, and the three laws. A datatype that satisfies the laws with return and bind implemented is a monad. That’s all that is required to be a valid typeclass instance of Monad!
See the monad laws here.
Don’t let intuition and a vague feeling suffice, keep learning!