14

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

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

在 hedis 中,定义Redis了一个monad 。从那里,您使用 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 包引入了一个 monadRedis来使用 redis 封装 IO 操作runRedis。相比之下, riak 包withConnection只接受一个接受 a 的函数Connection,并在 IO monad 中执行它。

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

4

2 回答 2

10

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

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

这将需要对用户代码进行彻底的更改。但是 Redis 包可以在不破坏用户的情况下实现这样的改变。

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

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

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

于 2013-04-18T01:45:39.940 回答
1

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

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

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

此外,如果您现在需要使用 2 个 redis 数据库,您将很难弄清楚要在哪里提升哪些操作:)

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

于 2013-04-22T05:47:27.353 回答