整理单子 - 将单子变换器的应用转化为新类型单子。

10

我想将类似于 ExceptT a (StateT A M) 的东西,对于某个具体类型 A 和 monad M,封装成我的新自定义 monad。

首先我确定 StateT A M 在其他情况下经常出现,因此我决定最好单独用一个 monad M1 将其封装起来,然后再将 ExceptT a M1 封装进 M2 中。

期望的属性是让 M1M2 成为 MonadState 类的实例以及 M 的类(假设它被称为 MyMonadClass )的实例。此外,M2 应该是 MonadError 的实例。

首先,我从简单的类型同义词开始:

type MyState    = StateT A M
type MyBranch a = ExceptT a MyState

然后我想首先勾勒出实例声明(不实现实例),这就是我第一次卡住的地方。 instance MonadState A (MyState) 似乎不是正确的语法。 我想我必须创建 newtype MyState' a = StateT a M,然后 type MyState = MyState A(在不必要使用语言扩展的情况下)。

然而,一旦我开始将同义词转换为 newtype 声明,我开始失去与 StateT A MExceptT ... 类型的联系。

newtype MyState' s a = MyState' { runMyState :: s -> (s, a) }
type MyState = MyState' A
newtype MyBranch e a = MyBranch { runMyBranch :: MyState (Either e a) }

现在已经实现的变压器消失了,我认为我正在尝试做一些没有太多意义的事情。因此,我的问题是:如何正确地将这种行为包装成新的复合单子,使得底层可以访问,从而避免不必要的提升并保持清晰和组织良好。

1个回答

12

通常的做法是为您的完整转换器堆栈定义一个新类型。

data A = A
data E = E

newtype MyBranchT m a = MyBranchT { getMyBranchT :: ExceptT E (StateT A m) a }

如果堆栈中有任何单独增加有意义的新功能的部分,您还需要为这些部分定义新类型。

然后,您可以使用GeneralizedNewtypeDeriving为各种单子类获取实例。

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- base
import Control.Applicative
import Control.Monad

-- transformers
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
import Control.Monad.Trans.State.Lazy

-- mtl classes
import Control.Monad.Cont.Class
import Control.Monad.Error.Class
import Control.Monad.Reader.Class
import Control.Monad.State.Class
import Control.Monad.Writer.Class

data A = A
data E = E

newtype MyBranchT m a = MyBranchT { getMyBranchT :: ExceptT E (StateT A m) a }
    deriving (Functor, Applicative, Monad, MonadIO,     -- classes from base and transformers
              {- Alternative, MonadPlus, -}             -- if E is a monoid
              MonadState A, MonadError E,               -- classes from mtl that MyBranchT provides
              MonadCont, MonadReader r, MonadWriter w)  -- classes from mtl that might be available from m

你将不得不手写 MonadTrans 实例。对于堆栈中的每个变换器,lift 都是只需要执行一次 lift

instance MonadTrans MyBranchT where
    lift = MyBranchT . lift . lift

如果堆栈中有任何增加了有意义的新功能 X ,您还需要为这些功能定义一个新的 MonadX 类,如果可能的话,为每个单子转换器(StateTExceptTContT 等)编写 MonadX 实例,并为您的转换器堆栈(MyBranchT)派生 MonadX 实例。

通常,您还会为 MyBranchT Identity 制作一个类型同义词和用于运行 MyBranchTrunMyBranch 的函数。

import Data.Functor.Identity

type MyBranch a = MyBranchT Identity a

runMyBranchT :: MyBranchT m a -> A -> m (Either E a, A)
runMyBranchT mb s = runStateT (runExceptT (getMyBranchT mb)) s

runMyBranch :: MyBranch a -> A -> (Either E a, A)
runMyBranch mb s = runIdentity $ runMyBranchT mb s

2
非常感谢,这是一个很好的答案。我是否可以不使用语言扩展,比如GeneralizedNewtypeDeriving - jakubdaniel
2
@JakubDaniel 当然,你可以手写所有实例。但你不想这样做(而且可能会搞砸一个),因此使用“GeneralizedNewtypeDeriving”。 - Cirdec
2
要查看实际推导出的代码,请在GHCi中运行命令 :set -ddump-deriv,它会显示 GHC 为您编写的所有代码。data AB = A | B deriving Eq 转储 instance GHC.Classes.Eq Ghci5.AB where (GHC.Classes.==) Ghci5.A Ghci5.A = GHC.Types.True; (GHC.Classes.==) Ghci5.B Ghci5.B = GHC.Types.True; (GHC.Classes.==) _ _ = GHC.Types.False - Iceland_jack
1
@Cirdec 我试着手写实例,但失败惨了。谢谢! - Bartek Banachewicz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接