何时(以及何时不)定义Monad



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

我举了两个haskell库的例子,它们的目的是与数据库后端进行IO。我举的例子是riak IO的riak,和Redis IO的heis。

在hedis中,定义了一个Redis单子。从那里,你运行IO与redis:

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单子上附加单子实例来完成的:

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封装IO操作。相比之下,withConnection中的riak包只是接受一个接受Connection的函数,并在IO单子中执行它。

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

对我来说,这一切都是关于封装和保护用户免受未来实现更改的影响。正如Casey所指出的,这两者现在大致相当——基本上是一个Reader Connection单子。但想象一下,如果未来发生不确定的变化,这些公司将如何表现。如果两个包最终都决定用户需要状态单子接口而不是阅读器,该怎么办?如果发生这种情况,riak的withConnection函数将更改为这样的类型签名:

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

这将需要对用户代码进行全面更改。但是Redis包可以在不影响用户的情况下完成这样的改变。

现在,有人可能会说这个假设的场景是非常不现实的,不是你需要计划的。在这两个特殊的例子中,这可能是对的。但是所有的项目都会随着时间的推移而发展,并且经常以不可预见的方式发展。定义自己的单子可以让你对用户隐藏内部实现细节,并提供一个通过未来的更改更稳定的接口。 当以这种方式声明

时,有些人可能会认为定义自己的monad是更好的方法。但我认为情况并非总是如此。(镜头库可能是一个很好的反例。)定义一个新的单子是有代价的。如果您使用的是单子转换器,它可能会造成性能损失。在其他情况下,API可能会变得更加冗长。Haskell非常好,让你保持语法非常小,在这种特殊情况下,差异不是很大——可能是一些liftIO的redis和一些lambdas的riak。

软件设计很少是一成不变的。你很少能自信地说什么时候定义自己的单子,什么时候不定义单子。但我们可以意识到所涉及的权衡,以帮助我们在遇到个别情况时评估。

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

例如,

hdbc也可以在普通IO单子中工作。

Monad for redis库没有带来任何有用的东西。它唯一实现的是摆脱一个函数参数(连接)。但是你要为它在redis单子内提升每个IO操作付出代价。

如果你需要同时使用2个redis数据库,你将很难确定哪些操作需要在哪里执行:)

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

最新更新