何时(何时没有)定义Monad

时间:2013-04-17 16:33:55

标签: haskell redis riak

这是一个与用于为Haskell库定义自己的Monad实例的API设计实践相关的问题。定义Monad实例似乎是隔离DSL的好方法,例如Par monad in monad-par,hdph;分布式流程中的Process; Eval并行等......

我拿两个haskell库的例子,其目的是使用数据库后端的IO。我采用的示例是Riak IO为riak,Redis IO为hedis

在hedis,Redis monad is defined。从那里,您使用redis运行IO:

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

在riak中,情况有所不同:

create :: Client -> Int -> NominalDiffTime -> Int -> IO Pool
ping :: Connection -> IO ()
withConnection :: Pool -> (Connection -> IO a) -> IO a

example = do
  conn <- connect defaultClient
  ping conn

runRedis的文档说:“runRedis的每次调用都从连接池获取网络连接并运行给定的Redis操作。因此,当来自池的所有连接都在时,对runRedis的调用可能会阻塞用“。。但是,riak包还实现了连接池。这是在IO monad之上没有额外的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

因此,两个软件包之间的类比归结为这两个函数:

runRedis       :: Connection -> Redis a -> IO a
withConnection :: Pool -> (Connection -> IO a) -> IO a

据我所知,hedis软件包引入了monad Redis,使用runRedis封装了使用redis的IO操作。相比之下,withConnection中的riak包只需要一个Connection的函数,并在IO monad中执行它。

那么,定义自己的Monad实例和Monad堆栈的动机是什么?为什么riak和redis包的方法不同?

2 个答案:

答案 0 :(得分:10)

对我而言,这完全取决于封装和保护用户免受未来实施变更的影响。正如凯西指出的那样,这两个现在大致相同 - 基本上是Reader Connection monad。但想象一下这些行为将如何受到不确定变化的影响。如果两个软件包最终决定用户需要状态monad接口而不是读者,该怎么办?如果发生这种情况,riak的withConnection函数将更改为类似签名:

withConnection :: Pool -> (Connection -> IO (a, Connection)) -> IO a

这需要对用户代码进行全面更改。但Redis软件包可以在不破坏用户的情况下实现这样的改变。

现在,有人可能会争辩说这个假设情景非常不现实,而且你不需要计划。在这两个特殊情况下,这可能是真的。但是所有项目都会随着时间的推移而发展,并且经常以不可预见的方式发展。定义您自己的monad允许您隐藏用户的内部实现细节,并提供一个在未来的更改中更稳定的界面。

当以这种方式陈述时,有些人可能会认为定义自己的monad是优越的方法。但我不认为总是这样。 (lens库可以作为一个可能很好的反例来考虑。)定义一个新的monad会产生成本。如果你使用monad变换器,它可能会造成性能损失。在其他情况下,API最终可能会更加冗长。 Haskell非常好,让你保持语法非常小,在这种特殊情况下,差异不是很大 - 可能是redis的几个liftIO和riak的一些lambdas。

软件设计很少被切割和干燥。很少有人能够自信地说何时何地不定义自己的monad。但是,我们可以意识到所涉及的权衡,以帮助我们在遇到个人情况时对其进行评估。

答案 1 :(得分:1)

在这种情况下,我认为实施monad是一个错误。它正在帮助java开发人员实现各种设计模式,只是为了拥有它们。

例如,hdbc也适用于普通的IO monad。

Monad for redis库没有带来任何有用的东西。它唯一能实现的是摆脱一个函数参数(连接)。但是你在redis monad里面支付它来解除每个IO操作。

此外,如果您现在需要使用2个redis数据库,那么您将很难找出哪些操作可以解除其中:)

实现monad的唯一原因是创建一个新的DSL。如你所见,hedis没有创建新的DSL。它的操作与任何其他数据库库完全相同。因此,hedis中的monad是肤浅的,没有道理。