Making Sense of Monads!

We have a special announcement this week! We have a new course available at Monday Morning Haskell Academy! The course is called Making Sense of Monads, and as you might expect, it tackles the concept of monads! It's a short, one module course, but it goes into a good amount of detail about this vital topic, and includes a couple challenge projects at the end. Sign up here!. If you subscribe to our mailing list, you can get a special discount on this and our other courses!

In addition to this, we've also got some new blog content! Once again, there's a video, but you can also follow along by scrolling down!

Last week we discussed the function application operator, which I used for a long time as a syntactic crutch without really understanding it. This week we'll take another look at a function-related concept, but we'll relate it to our new monads course. We're going to explore the "function monad". That is, a single-argument function can act as a monad and call other functions which take the same input in a monadic fashion. Let's see how this works!

The Structure of Do Syntax

Let's start by considering a function in a more familiar monad like IO. Here's a function that queries the user for their name and writes it to a file.

ioFunc :: IO ()
ioFunc = do
  putStrLn "Please enter your name"
  name <- getLine
  handle <- openFile "name.txt" WriteMode
  hPutStrLn handle name
  hClose handle

Do syntax has a discernable structure. We can see this when we add all the type signatures in:

ioFunc :: IO String
ioFunc = do
  putStrLn "Please enter your name" :: IO ()
  (name :: String) <- getLine :: IO String
  (handle :: Handle) <- openFile "name.txt" WriteMode :: IO Handle
  hPutStrLn handle name :: IO ()
  hClose handle :: IO ()
  return name :: IO String

Certain lines have no result (returning ()), so they are just IO () expressions. Other lines "get" values using <-. For these lines, the right side is an expression IO a and the left side is the unwrapped result, of type a. And then the final line is monadic and must match the type of the complete expression (IO String in this case) without unwrapping it's result.

Here's how we might expression that pattern in more general terms, with a generic monad m:

combine :: a -> b -> Result

monadFunc :: m Result
monadFunc = do
  (result1 :: a) <- exp1 :: m a
  (result2 :: b) <- exp2 :: m b
  exp3 :: m ()
  return (combine result1 result2) :: m Result

Using a Function

It turns out there is also a monad instance for (->) r, which is to say, a function taking some type r. To make this more concrete, let's suppose the r type is Int. Let's rewrite that generic expression, but instead of expressions like m Result, we'll instead have Int -> Result.

monadFunc :: Int -> Result
monadFunc = do
  (result1 :: a) <- exp1 :: Int -> a
  (result2 :: b) <- exp2 :: Int -> b
  exp3 :: Int -> ()
  return (combine result1 result2) :: Int -> Result

So on the right, we see an expression "in the monad", like Int -> a. Then on the left is the "unwrapped" expression, of type a! Let's make this even more concrete! We'll remove exp3 since the function monad can't have any side effects, so a function returning () can't do anything the way IO () can.

monadFunc :: Int -> Int
monadFunc = do
  result1 <- (+) 5
  result2 <- (+) 11
  return (result1 * result2)

And we can run this function like we could run any other Int -> Int function! We don't need a run function like some other functions (Reader, State, etc.).

>> monadFunc 5
160
>> monadFunc 10
315

Each line of the function uses the same input argument for its own input!

Now what does return mean in this monadic context? Well the final expression we have there is a constant expression. It must be a function to fit within the monad, but it doesn't care about the second input to the function. Well this is the exact definition of the const expression!

const :: a -> b -> a
const a _ = a -- Ignore second input!

So we could replace return with const and it would still work!

monadFunc :: Int -> Int
monadFunc = do
  result1 <- (+) 5
  result2 <- (+) 11
  const (result1 * result2)

Now we could also use the implicit input for the last line! Here's an example where we don't use return:

monadFunc :: Int -> Int
monadFunc = do
  result1 <- (+) 5
  result2 <- (+) 11
  (+) (result1 * result2)
...

>> monadFunc 5
165
>> monadFunc 10
325

And of course, we could define multiple functions in this monad and call them from one another:

monadFunc2 :: Int -> String
monadFunc2 = do
  result <- monadFunc
  showInput <- show
  const (show result ++ " " ++ showInput)

Like a Reader?

So let's think about this monad more abstractly. This monadic unit gives us access to a single read-only input for each computation. Does this sound familiar to you? This is actually exactly like the Reader monad! And, in fact, there's an instance of the MonadReader typeclass for the function monad!

instance MonadReader r ((->) r) where
...

So without changing anything, we can actually call Reader functions like local! Let's rewrite our function from above, except double the input for the call to monadFunc:

monadFunc2 :: Int -> String
monadFunc2 = do
  result <- local (*2) monadFunc
  showInput <- show
  const (show result ++ " " ++ showInput)

...

>> func2 5
"325 5"
>> func2 10
"795 10"

This isomorphism is one reason why you might not use the function monad explicitly so much. The Reader monad is a bit more canonical and natural. But, it's still useful to have this connection in mind, because it might be useful if you have a lot of different functions that take the same input!

If you're not super familiar with monads yet, hopefully this piqued your interest! To learn more, you can sign up for Making Sense of Monads! And if you subscribe to Monday Morning Haskell you can get a special discount, so don't wait!

Previous
Previous

Hidden Identity: Using the Identity Monad

Next
Next

Function Application: Using the Dollar Sign ($)