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
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
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:
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.
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:
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
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
runMaybeT :: MaybeT m a -> m (Maybe a); turned out the constructor is just the reverse:
MaybeT :: m (Maybe a) -> MaybeT m a. This means:
Long story short, the much improved version of
1 2 3 4 5 6 7 8 9 10 11
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!