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

Haskell 如何将上下文和'IO'单子隐藏到另一个单子中?,haskell,monads,Haskell,Monads,我正在尝试使用HDBC和Haskell.GI实现一个小型桌面应用程序。我使用glade构建窗口和对话框,并使用GtkBuilder加载它们。在实现了两个场景之后,我始终使用相同的模式,在do块中组成“动作”,其签名为: Connection -> Builder -> a -> IO b 这些“操作”是在IOmonad的上下文中组成的,主要问题是我必须将连接和构建器到处传递。我预见到的另一个问题是,如果我想向我的应用程序添加另一个外部依赖项(例如,访问图像扫描仪),我必须更改

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

Connection -> Builder -> a -> IO b
这些“操作”是在
IO
monad的上下文中组成的,主要问题是我必须将
连接
构建器
到处传递。我预见到的另一个问题是,如果我想向我的应用程序添加另一个外部依赖项(例如,访问图像扫描仪),我必须更改我所有“操作”的签名,更重要的是,更改它们的算术

我能做的是:我能定义一个类型同义词:

type Action a b = Connection -> Builder -> a -> IO b
我还可以创建一个命名元组来消除算术性问题:

data Context =
    Context {
        conn :: Connection,
        builder :: Builder}
但是,这仍然不能解决这样一个事实:每次我想访问数据库时,我都必须调用
(conn ctx)
,或者在每个操作中使用
let
绑定

我觉得最理想的是制作我自己的monad,在其中我可以编写我的动作,我不会明确地谈论我的
连接
构建器

知道
IO
已经是一个单子,我如何定义这样的单子呢

顺便说一句,它和
状态
单子有什么关系吗

[…]主要的问题是我必须通过我的
连接
构建器

因此,这些都是您(反复)阅读的“环境”的一部分。这就是
阅读器的作用。包
mtl
包含monad transformer,它为基本monad添加了读卡器功能,在您的例子中是
IO

演示: 假设一个简单的动作,比如

no_action :: Connection -> Builder -> Int -> IO Int
no_action _ _ i = return (i + 1)
您可以将其放入一个新的Monad中,该Monad类似于
IO
,但通过定义
上下文并应用Monad转换器,可以访问连接和生成器:

data Context = Context { connection :: Connection
                       , builder :: Builder }
type CBIO b = ReaderT Context IO b
将您的动作提升到这个新的(组合的)单子中,应该有一个单独的功能:

liftCBIO :: (Connection -> Builder -> a -> IO b) -> (a -> CBIO b)
liftCBIO f v = do
    context <- ask
    liftIO (f (connection context) (builder context) v)
。。。和
cbio\u no\u action num

要实际运行新的monad,您需要使用
runReaderT
。。但这也应该有一个更好的名字:

runWithInIO = flip runReaderT
如果愿意,您还可以将其更改为包含构建
上下文

使用上面的方法看起来是这样的:

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
main=do

听起来你可能想看看读卡器monad。(它确实类似于状态单子,但状态是只读的。)@RobinZigmond:好的。你的意思似乎是:如果我像问题中那样定义
上下文
类型,并定义
类型Action=ReaderT Context IO
,我可以做
(reader conn)
(reader builder)
liftIO$do…
?不完全-
(ReaderT Context IO)b
,模
新类型
包装,相当于
Context->IO b
——因此它不包括
a
参数。当然,您可以重新排列参数的顺序并使用函数
a->ReaderT Context IO b
,这样
(新)类型Action a b=a->ReaderT Context IO b
将是一个合适的单元可组合函数类型。(monad是
ReaderT Context IO
)我想我现在明白了。使用您自己的monad,您可以定义自己的助手,如
getConnection::Action a Connection
。您可能还想定义一个
MonadIO(Action a)
实例,这样您就可以在自定义monad中运行
liftIO(putStrLn“hello”)
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