我正在尝试使用HDBC
和Haskell.GI
实现一个小型桌面应用程序。我使用glade构建我的窗口和对话框,并加载它们GtkBuilder
。在实现几个场景之后,我最终在整个过程中使用相同的模式,在do
具有以下签名的块中编写"操作":
Connection -> Builder -> a -> IO b
这些"行动"是在IO
monad的背景下组成的,主要问题是我必须通过我的Connection
和Builder
。我预见到的另一个问题是,如果我想向我的应用程序添加另一个外部依赖项(例如,访问图像扫描仪),我将不得不更改所有"操作"的签名,更重要的是,它们的arity。
我能做什么:我可以定义一个类型同义词:
type Action a b = Connection -> Builder -> a -> IO b
我还可以创建一个命名元组来消除 arity 问题:
data Context =
Context {
conn :: Connection,
builder :: Builder}
但是,这仍然不能解决这样一个事实,即每次我想访问数据库时,我都必须在每个操作中调用(conn ctx)
或使用let
绑定。
我觉得最理想的做法是制作我自己的monad,我可以在其中创作我的行为,我不会明确地谈论我的Connection
或Builder
价值观。
知道IO
已经是单子了,我该如何定义这样的单子?
顺便说一句,它与State
monad 有什么关系吗?
[..] 主要问题是我必须通过我的
Connection
和Builder
。
因此,这些是您(反复)阅读的"环境"的一部分。 这就是Reader
monad 的用途。 包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 的完整演示)