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

时间:2019-04-16 10:50:26

标签: haskell monads

我正在尝试使用HDBCHaskell.GI实现一个小型桌面应用程序。我使用林间空地构建窗口和对话框,并使用GtkBuilder加载它们。在实施了几种方案之后,我最终在整个过程中使用了相同的模式,在do块中编写了具有以下特征的“动作”:

Connection -> Builder -> a -> IO b

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

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

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

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

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

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

我认为理想的情况是制作自己的monad,在其中可以构成自己的动作,而我不会明确谈论我的ConnectionBuilder值。

知道IO已经是单子之后,我将如何定义这样的单子?

顺便说一句,与State单子有什么关系?

1 个答案:

答案 0 :(得分:1)

[..] the main problem being that I have to pass my Connection and Builder all around.

So these are part of an "environment" that You read from (repeatedly). That's what the Reader monad is for. The package mtl contains the monad transformer ReaderT which adds reader functionality to a base monad, in Your case IO.

Demo:

Assuming a simple action, like ..

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

You can put this into a new Monad which is like IO but with access to both connection and builder by defining a Context and applying the monad transformer:

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

Lifting Your actions into this new (combined) monad deserves a function on its own:

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

Then You can either always write (liftCBIO no_action) num or ...

cbio_no_action = liftCBIO no_action

... and cbio_no_action num.

To actually run Your new monad You'd use runReaderT .. but this also deserves a better name:

runWithInIO = flip runReaderT

You could also change this to incorporate building the Context, if You like.

Using the above then looks like that:

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)