如何将上下文和"IO"单子隐藏到另一个单体中?



我正在尝试使用HDBCHaskell.GI实现一个小型桌面应用程序。我使用glade构建我的窗口和对话框,并加载它们GtkBuilder。在实现几个场景之后,我最终在整个过程中使用相同的模式,在do具有以下签名的块中编写"操作":

Connection -> Builder -> a -> IO b

这些"行动"是在IOmonad的背景下组成的,主要问题是我必须通过我的ConnectionBuilder。我预见到的另一个问题是,如果我想向我的应用程序添加另一个外部依赖项(例如,访问图像扫描仪),我将不得不更改所有"操作"的签名,更重要的是,它们的arity。

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

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

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

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

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

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

知道IO已经是单子了,我该如何定义这样的单子?

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

[..] 主要问题是我必须通过我的ConnectionBuilder

因此,这些是您(反复)阅读的"环境"的一部分。 这就是Readermonad 的用途。 包mtl包含 monad 变压器ReaderT,该变压器将读取器功能添加到基本 monad,在 Your caseIO

演示:

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

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

你可以把它放到一个新的Monad中,它就像IO一样,但可以通过定义一个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

(关于 ideone 的完整演示)

最新更新