Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When (and when not) to define a Monad

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?

like image 446
Rob Stewart Avatar asked Apr 17 '13 16:04

Rob Stewart


People also ask

What a monad is not?

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.

What are monads explain with example?

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.

What is monad condition?

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 .


2 Answers

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.

like image 107
mightybyte Avatar answered Oct 18 '22 23:10

mightybyte


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.

like image 1
Vagif Verdi Avatar answered Oct 18 '22 23:10

Vagif Verdi