KISS

Keep It Simple Stupid

My week in Haskell: monad transformers

| comments

This will likely be a less coherent post than usual. I’ll try to describe my investigations and new experience in Haskell this week. I’m only the beginner in this field, but these thoughts might be helpful to other beginners too. You can ask questions in the comments.

I have a tiny program logdl that downloads log files from an iOS text editor via HTTP using the http-client package. I was thinking about extending it to support FTP downloads from an Android device as well and of course there is a Haskell library for an FTP client, for example ftp-client. There is a caveat with that library: all FTP operations must be done within the withFTP scope, bracket-style, which is different from creating and using a manager in the HTTP library — so this difference needs to be dealt with.

The current code in my program looks like an imperative mess in Main.hs, I admit that it’s far from perfect; to support a different transport I shouldn’t need to change the business logic, but right now those two parts aren’t separate. The solution I see here is “depend on the abstraction, not on the concrete implementation” a.k.a. dependency injection. I know how I’d do it in swift, but what about Haskell?

Dependency injection in Haskell?

Based on my small practical FP experience, I thought that dependency injection, as well as everything else in FP, is done with functions. I was lucky to come across Mark Seemann’s blog blog.ploeh.dk, where he describes a lot of ideas and implementations related to functional programming. The From dependency injection to dependency rejection series is important for this post. I had vaguely similar ideas: you can extract a dependency and inject it as a parameter, but if that dependency talks with the real-world, it will have an IO-wrapped return type, which in turn infects the function that uses it. Say:

1
2
3
4
5
-- the original function to download a file by name and dealing with paginated response, it's in IO since it communicates via HTTP
downloadFile :: Filename -> IO ByteString

-- the first parameter is an extracted dependency now: a function that know only how to get something by a `URLRequest`; note that both the dependency and the business logic functions return IO values
downloadFile' :: (URLRequest -> IO Response) -> Filename -> IO ByteString

However we (let me shamelessly count myself as a Haskell programmer now :)) don’t like IO much because its scope it too wide: the downloadFile' function is allowed to do anything. My next thought was, well what if we abstract the IO out by leaving just a monad in the signature like this:

1
downloadFile'' :: Monad m => (URLRequest -> m Response) -> Filename -> m ByteString

In which case, the downloadFile'' function doesn’t care how the dependency works, it only cares that it’s a monad because it has to deal with paginated responses (if there is a next page link in the response, then it fetches that one and so on and concatenates the results). Of course, it won’t work with this function signature since just any monad can’t download a file. I didn’t think of the next step then.

In cases where the pure-impure-pure sandwich doesn’t work, Mark’s suggestion is to model pure interactions with free monads. However I didn’t jump into that field yet because the great The Book of Monads had at least three different styles of creating custom monads. I decided to start with the mtl-style custom monad, with an example in the comments to Pure times in Haskell.

HTTP monad

Note: all the following code isn’t published anywhere yet. I will release a cleaned-up version later.

Now I was trying to create my own monad to work with remote text files, which could work via HTTP or FTP. But the lower level first:

1
2
class HTTP m where
  get :: HTTPPath -> m (Maybe C.ByteString)

A sample program that downloads an index page, gets a list of files and then downloads the first one:

1
2
3
4
5
6
program :: (HTTP m, Monad m) => m String
program = do
  files <- fmap (lines . C.unpack) <$> get (HTTPPath "")
  let firstFilename = head . fromMaybe [] $ files
  firstFile <- get (HTTPPath firstFilename)
  return $ maybe ("No file " <> firstFilename) C.unpack firstFile

Since get may not return anything (Nothing), I have to fmap twice over the index page result: fmap (lines . C.unpack) <$> get … to transform the inner C.ByteString. Also this version is unsafe because if there is no index page, the program will terminate trying to evaluate firstFilename. I was then trying to do something similar and stumbled upon the same issue with dealing with the inner Maybe values. This seemed like too much hassle and there should be a better way in Haskell! For some reason I thought about monad transformers, read a few articles and it was the right approach! I didn’t understand it that fast of course, turned out I didn’t know how they work.

But at first, I was confused as to how to convert a Maybe value to a MaybeT value. The “getter” for MaybeT is runMaybeT :: MaybeT m a -> m (Maybe a); turned out the constructor is just the reverse: MaybeT :: m (Maybe a) -> MaybeT m a. This means:

1
2
λ> mValue = Just 42 :: Maybe Int
λ> mValueT = MaybeT . pure $ mValue :: Applicative m => MaybeT m Int

Long story short, the much improved version of program is:

1
2
3
4
5
6
7
8
9
10
11
getM :: HTTP m => HTTPPath -> MaybeT m C.ByteString
getM = MaybeT . get

safeHeadM :: Applicative m => [a] -> MaybeT m a
safeHeadM = MaybeT . pure . safeHead

program :: (HTTP m, Monad m) => m String
program = fmap (fromMaybe "error!") . runMaybeT $ do
  root <- lines . C.unpack <$> getM (HTTPPath "")
  firstFilename <- safeHeadM root
  C.unpack <$> getM (HTTPPath firstFilename)

The do block here is not of type m String, but MaybeT m String. It seems obvious now, but it wasn’t when I had type errors all the time. If you use type inference everywhere inside your non-trivial functions, then a slight change may drastically change the types of your values and you may see bizarre errors. If you have those, remember: “follow the types”, make sure you understand what type each expression should be and what it actually is in the code, however slow that analysis may be at the beginning.

So the amazing thing in this code is that I can use the get function (from my HTTP monad) and not have to care about any of the missing results. If any of the values is Nothing, the entire block will return Nothing, exactly what I wanted. This is all in the >>= operator for that more advanced monad.

It was a long post and I know a little bit more now, about monad transformers and custom monads too.

ps. The people who discovered (invented?) and implemented monads and monad transformers are geniuses!

Comments