This is a question relating to the API design practises for defining your own Monad instances for Haskell libraries. Defining Monad instances seems to be a good way to isolate DSL's e.g. Par
monad in monad-par, hdph; Process
in distributed-process; Eval
in parallel etc...
I take two examples of haskell libraries, whose purpose is to IO with database backends. The examples I take are riak for Riak IO, and hedis for Redis IO.
In hedis, a Redis
monad is defined. From there, you run IO with redis as:
data Redis a -- instance Monad Redis
runRedis :: Connection -> Redis a -> IO a
class Monad m => MonadRedis m
class MonadRedis m => RedisCtx m f | m -> f
set :: RedisCtx m f => ByteString -> ByteString -> m (f Status)
example = do
conn <- connect defaultConnectInfo
runRedis conn $ do
set "hello" "world"
world <- get "hello"
liftIO $ print world
In riak, things are different:
create :: Client -> Int -> NominalDiffTime -> Int -> IO Pool
ping :: Connection -> IO ()
withConnection :: Pool -> (Connection -> IO a) -> IO a
example = do
conn <- connect defaultClient
ping conn
The documentation for runRedis
says: "Each call of runRedis takes a network connection from the Connection pool and runs the given Redis action. Calls to runRedis may thus block while all connections from the pool are in use.". However, the riak package also implements connection pools. This is done without additional monad instances on top of the IO monad:
create :: Client-> Int -> NominalDiffTime -> Int -> IO Pool
withConnection :: Pool -> (Connection -> IO a) -> IO a
exampleWithPool = do
pool <- create defaultClient 1 0.5 1
withConnection pool $ \conn -> ping conn
So, the analogy between the two packages boils down to these two functions:
runRedis :: Connection -> Redis a -> IO a
withConnection :: Pool -> (Connection -> IO a) -> IO a
As far as I can tell, the hedis package introduces a monad Redis
to encapsulate IO actions with redis using runRedis
. In contrast the riak package in withConnection
simply takes a function that takes a Connection
, and executes it in the IO monad.
So, what are the motivations for defining your own Monad instances and Monad stacks? Why has the riak and redis packages differed in their approach to this?
Monads are not about ordering/sequencing But this is misleading. Just as you can use monads for state, or strictness, you can use them to order computations. But there are also commutative monads, like Reader, that don't order anything. So ordering is not in any way essential to what a monad is.
In functional programming, a monad is a software design pattern with a structure that combines program fragments (functions) and wraps their return values in a type with additional computation.
A monad is essentially just a functor T with two extra methods, join , of type T (T a) -> T a , and unit (sometimes called return , fork , or pure ) of type a -> T a .
For me it's all about encapsulation and protecting users against future implementation changes. As Casey has pointed out, these two are roughly equivalent right now--basically a Reader Connection
monad. But imagine how these will behave subject to uncertain changes down the road. What if both packages end up deciding that the user needs a state monad interface instead of a reader? If that happens, riak's withConnection
function will change to a type signature like this:
withConnection :: Pool -> (Connection -> IO (a, Connection)) -> IO a
This will require sweeping changes to user code. But the Redis package could pull off such a change without breaking its users.
Now, one might argue that this hypothetical scenario is very unrealistic and not something you need to plan for. And in these two particular cases, that may be true. But all projects evolve over time, and frequently in unforeseen ways. Defining your own monad allows you to hide internal implementation details from your users and provide an interface that is more stable through future changes.
When stated this way, some might conclude that defining your own monad is the superior approach. But I don't think that is always the case. (The lens library comes to mind as a potentially good counter-example.) Defining a new monad has costs. If you're using monad transformers, it can impose a performance penalty. In other cases the API might end up being more verbose. Haskell is very good letting you keep the syntax very minimal and in this particular case, the difference isn't very big--probably a few liftIO
's for redis and a few lambdas for riak.
Software design is rarely cut and dried. It's rare that you'll be able to confidently say when and when not to define your own monad. But we can become aware of the tradeoffs involved to aid our assessment of individual situations as we encounter them.
In this case i think implementing monad was a mistake. It's aking java developers implementing all kinds of design patterns just for the sake of having them.
hdbc for example also works in plain IO monad.
Monad for redis library does not bring anything useful. The only thing it achieves is to get rid of one function argument (connection). But you pay for it lifting every IO operation while inside redis monad.
Also if you ever need to work with 2 redis databases now you gonna have a hard time trying to figure out which operations to lift where :)
The only reason to implement a monad is to create a new DSL. As you see hedis did not create a new DSL. Its operations are exactly like any other database library. Therefore monad in hedis is superficial and is not justified.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With