When (and when not) to define a Monad

haskell, redis, riak

Solution

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.

Problem

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?

Original source