【问题标题】:How to hide context and the `IO` monad into another monad?如何将上下文和 `IO` monad 隐藏到另一个 monad 中?
【发布时间】:2019-04-16 10:50:25
【问题描述】:

我正在尝试使用HDBCHaskell.GI 实现一个小型桌面应用程序。我使用 glade 构建我的窗口和对话框,并使用 GtkBuilder 加载它们。在实现了几个场景之后,我最终使用了相同的模式,在 do 块中组成“动作”,其签名为:

Connection -> Builder -> a -> IO b

这些“动作”在IO monad 的上下文中构成,主要问题是我必须到处传递我的ConnectionBuilder。我预见的另一个问题是,如果我想向我的应用程序添加另一个外部依赖项(例如,访问图像扫描仪),我将不得不更改所有“动作”的签名,更重要的是,它们的数量。

我能做什么:我可以定义一个类型同义词:

type Action a b = Connection -> Builder -> a -> IO b

我还可以创建一个命名元组来消除元组问题:

data Context =
    Context {
        conn :: Connection,
        builder :: Builder}

但这仍然不能解决这样一个事实,即每次我想访问数据库时,我都必须调用(conn ctx) 或在每个操作中使用let 绑定。

我觉得最理想的做法是制作我自己的 monad,我可以在其中组合我的行为,并且我不会明确谈论我的 ConnectionBuilder 值。

知道IO 已经是一个monad,我该如何定义这样的monad?

顺便说一句,它与State monad 有什么关系吗?

【问题讨论】:

  • 听起来你可能想看看 Reader monad。 (它确实类似于 State monad,但 state 是只读的。)
  • @RobinZigmond:好的。你的意思是:如果我在问题中定义Context类型,并定义type Action = ReaderT Context IO,我可以做(reader conn)(reader builder)liftIO $ do ...
  • 不完全 - (ReaderT Context IO) b,模 newtype 包装,相当于 Context -> IO b - 所以它不包括你的 a 参数。您当然可以重新排列参数的顺序并使用函数a -> ReaderT Context IO b
  • 所以(new)type Action a b = a -> ReaderT Context IO b将是一个适当的单子可组合函数类型。 (单子是ReaderT Context IO)我想我现在明白了。
  • 使用自己的 monad,您可以定义自己的助手,例如 getConnection :: Action a Connection。您可能还想定义一个 MonadIO (Action a) 实例,以便您可以在自定义 monad 中运行 liftIO (putStrLn "hello") 之类的东西。

标签: haskell monads


【解决方案1】:

[..] 主要问题是我必须传递我的 ConnectionBuilder

因此,这些是您(反复)阅读的“环境”的一部分。这就是 Reader monad 的用途。包 mtl 包含 monad 转换器 ReaderT,它将阅读器功能添加到基本 monad,在您的情况下为 IO

演示:

假设一个简单的动作,比如 ..

no_action :: Connection -> Builder -> Int -> IO Int
no_action _ _ i = return (i + 1)

您可以将其放入一个类似于 IO 的新 Monad 中,但可以通过定义 Context 并应用 monad 转换器来访问连接和构建器:

data Context = Context { connection :: Connection
                       , builder :: Builder }
type CBIO b = ReaderT Context IO b

将你的动作提升到这个新的(组合的)monad 中值得拥有一个单独的函数:

liftCBIO :: (Connection -> Builder -> a -> IO b) -> (a -> CBIO b)
liftCBIO f v = do
    context <- ask
    liftIO (f (connection context) (builder context) v)

那么您可以随时写(liftCBIO no_action) num 或...

cbio_no_action = liftCBIO no_action

...和cbio_no_action num.

要真正运行你的新 monad,你会使用 runReaderT .. 但这也值得一个更好的名字:

runWithInIO = flip runReaderT

如果您愿意,您也可以将其更改为包含构建 Context

使用上面的然后看起来像这样:

main = do
    i <- runWithInIO (Context Connection Builder) $ do
        a <- cbio_no_action 20
        liftIO $ putStrLn "Halfway through!"
        b <- cbio_no_action 30
        return (a + b)
    putStrLn $ show i

(Full demo on ideone)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-29
    • 2019-10-29
    • 2012-08-14
    • 1970-01-01
    • 2020-03-12
    相关资源
    最近更新 更多