【问题标题】:Uses and examples of monad transformers?monad 转换器的用途和例子?
【发布时间】:2017-02-13 09:38:35
【问题描述】:

我正在讨论 Monad Transformers,我了解它们的主要作用是提供一个 monadic 容器来保存不同类型的 monad,它提供了一个通用接口,可以从中操作计算中的“嵌套”monad。

我尝试实现自己的转换器:

data CustomTransformer a = CustomTransformer

class TransformerClass m a where
  lift :: m a -> CustomTransformer (m a)

instance TransformerClass Maybe a where
 lift (Just a) = CustomerTransformer (Just a)

通过this 论文,我知道这是不正确的。他们的例子表明:

class MonadTrans r where
  lift :: Monad m => m a -> (r m) a

这会将动作 a 嵌套在 monad 转换器 r m 中。

我不明白 使用单子转换器如何帮助处理计算中的多个单子类型?谁能提供一个简单的解释和例子?

【问题讨论】:

    标签: haskell monads


    【解决方案1】:

    我发现了解这里起作用的种类很有帮助。

    首先,如您所知,monad类型构造函数 m :: * -> *return :: a -> m a(>>=) :: m a -> (a -> m b) -> m b 两个操作配对。

    class Monad (m :: * -> *) where
        return :: a -> m a
        (>>=) :: m a -> (a -> m b) -> m b
    

    monad 转换器 的想法是,它们是一种类型级别的函数,可以将一个 monad 转换为另一个 monad。因此,假设 monad 是单参数类型构造函数* -> *,则删除括号后,monad 转换器必须是(* -> *) -> (* -> *)(* -> *) -> * -> * 的类型。 monad 转换器是一个 双参数 类型,其第一个参数是一个 monad,第二个参数是一个值。

    更具体地说,monad 转换器是 t :: (* -> *) -> * -> * 类型,因此只要 m 是 monad,t m 也是 monad。我们还要求t mm 更大 monad,因为m 中的任何动作都可以嵌入t m 中。

    class MonadTrans t where
        transform :: Monad m :- Monad (t m)
        lift :: Monad m => m a -> t m a
    

    我在transform的定义中使用Kmett's constraints package中的the "entailment" operator :-transform 证明 mMonad 意味着 t mMonad。 (The version of MonadTrans in transformers 省略了 transform 成员,因为编写时 GHC 不支持 :- 运算符。)

    重要的是,t m a(又名(t m) a)的含义不同于t (m a)。前者是一个二参数类型t 应用于ma。后者是单参数类型t应用于m a

    一个非常简单的例子——因为我在我的手机上——monad 转换器的定义是什么样的例子:

    newtype IdentityT m a = IdentityT { runIdentityT :: m a }
    
    instance Monad m => Monad (IdentityT m) where
        return = IdentityT . return
        IdentityT m >>= f = IdentityT $ m >>= (runIdentityT . f)
    
    instance MonadTrans IdentityT where
        transform = Sub Dict
        lift = IdentityT
    

    注意IdentityT 是一个双参数数据类型;第一个参数m :: * -> *是一个monad,第二个参数a :: *是一个常规类型。

    ghci> :k IdentityT
    IdentityT :: (* -> *) -> * -> *
    

    【讨论】:

      【解决方案2】:

      Monad,是任何多态数据类型m a,它实现了 monad 操作,return>>=,并遵守 monad 法则。

      有些 monad 有一种特殊的形式,m 可以写成多态的 mT m',只要参数 m' 是一个 monad,它就会是一个 monad。我们可以像这样拆分的 Monad 是 monad 转换器。外部 monad mT,为内部 monad 添加了 monadic 效果。我们可以嵌套无限数量的 monad,因为内部的 m' 本身可能是一个 monad 转换器。

      由于 Maybe 是最简单的 monad 之一,我将重新发布来自 Transformers 的代码。

      定义表明 monad MaybeT m 主要是 monad m 的包装器。然而,m 不再是一个“纯粹的”monad,而是带有一个被 Maybe 效应污染的类型参数。

      newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
      

      现在是 Monad 实例的定义。

      instance (Monad m) => Monad (MaybeT m) where
          return = lift . return
          x >>= f = MaybeT $ do
              v <- runMaybeT x
              case v of
                  Nothing -> return Nothing
                  Just y  -> runMaybeT (f y)
      

      查看绑定操作&gt;&gt;=,重要的是要注意$ 后面的do 符号发生在m monad 中。内部monad通过runMaybeT x回收,monadic值绑定到v,触发m效果。然后,评估Maybe 状态,将f 应用于一个值(如果存在)并适当地包装。

      对我来说,混淆的一个来源是内部和外部 monad 的术语——如果我把它倒过来,我不会感到惊讶。转换后的 monad mT 实际上将自己投射到内部 monad m

      问题是关于lift,它对应于在外部monad 的纯上下文中运行内部monad 的能力。请注意,lift 或 MonadTrans 没有定义转换器,但表示如果 monad (t m) 是转换器,那么您应该能够将 m a 提升为纯 (t m) a

      对于我的示例,下面是一些程序的模型,其中用户 请求一些资源。函数userGetResource 询问用户名,然后针对某个注册表查询该名称,如果找到该名称,它将尝试获取用户的权限,如果授予用户权限,它将返回资源。有一系列 IO 操作可能会因Nothing 而失败。 MaybeT 帮助编写函数,使其更易于阅读和维护。特别注意liftuserGetResource 函数中的使用。因为它总是会返回一个字符串(baring catastrophe),所以这个函数被提升为MaybeT的纯Just形式。

      import Data.List(find)
      import Control.Monad (liftM)
      import Control.Monad.Trans.Maybe
      import Control.Monad.Trans.Class(lift)
      
      data User = User { userName :: String, hasCredentials :: Credentials }
      type Credentials = Bool
      type Token = ()
      type UserReg = [User]
      data Resource = Resource deriving Show
      
      userGetResource :: IO (Maybe Resource)
      userGetResource = runMaybeT $ do
        str <- lift $ do putStrLn "Who are you"
                         getLine
        usr <- MaybeT $ getUser str
        tok <- MaybeT $ getPermission usr
        MaybeT $ getResource tok
      
      
      getResource :: Token -> IO (Maybe Resource)
      getResource _ = return (Just Resource)
      
      userRegistry :: IO UserReg
      userRegistry = return [User "Alice" True, User "Bob" False]
      
      lookupUser :: String -> UserReg -> Maybe User
      lookupUser name = find ((name==) . userName)
      
      getUser :: String -> IO (Maybe User)
      getUser str = do
        reg <- userRegistry
        return $ lookupUser str reg
      
      getPermission :: User -> IO (Maybe Token)
      getPermission usr
        | hasCredentials usr = do
           tok <- generateToken
           return (Just tok)
        | otherwise          = return Nothing
      
      generateToken :: IO Token
      generateToken = doSomeUsefulIO
        where
          doSomeUsefulIO = return () 
      

      这是对userGetResource的多次调用的输出

      失败,“Sam”不在 userReg 中

      *MaybeTrans> userGetResource
      Who are you
      Sam
      Nothing
      

      成功,“爱丽丝”在注册表中并获得许可。

      *MaybeTrans> userGetResource 
      Who are you
      Alice
      Just Resource
      

      失败。 “Bob”在注册表中,但没有权限。

      *MaybeTrans> userGetResource
      Who are you
      Bob
      Nothing
      

      【讨论】:

        【解决方案3】:

        不同的单子会产生不同的“效果”,例如状态或不确定性。但是如果你想在 monad 中使用更多这些,你需要从头开始实现这样的 monad(这会很乏味)或者以某种方式堆叠 monad。变形金刚可以叠加各种单子的效果。 Transformers 通常是通过从提供您感兴趣的效果的 monad 中泛化获得的,提取效果同时可以将另一个 monad 插入其中。这是对其他 monad 的一种装饰,可以为它们添加您想要的 state/nondet/... 效果。查看软件包transformers 的hackage 以查看一些常见的examples

        Transformer 捕获关联 monad 的效果,但为您留出空间放入另一个 monad,如 StateT 的绑定 (&gt;&gt;=) 的定义:

        (>>=) :: StateT s m a -> (a -> StateT s m b) -> StateT s m b
        m >>= k = StateT $ \ s -> do
            (a, s') <- runStateT m s
            runStateT (k a) s'
        

        do 块在底层 monad 中“运行”(m,这些行通过 m 的绑定运算符 &gt;&gt;= 链接),因此“做自己的事情”,而 StateT保持侧面的状态。

        这样你可以堆叠更多的效果,每个转换器负责一个,并得到一个具有所有这些效果的单子。

        ExceptT e (StateT s (ListT IO))
        

        在这里,您表达了在IO 提供的“真实世界”环境中的“有状态”和“非确定性”计算中“抛出异常”的效果。请以这个为例,IO本身是有状态的,所以我不否认这个例子有点过分。

        请注意:具体的 monad 通常可以表示为最简单的 monad Identity 的转换,它本身并没有什么有趣的事情,就像 State s 是根据 StateT s Identity 实现的一样。

        【讨论】:

        • "请以这个为例,IO 本身是有状态的,所以我不否认这个例子有点过分" -- 我不认为StateT 超过IO是过度的。除了IO 之外,保留一层纯状态并没有错,而且它往往比处理可变引用更令人愉快。
        • 你是对的,这就是我把它放在那里的原因,这只是一个人为的例子,因为我没有想到真正的用例
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-11-27
        • 2017-12-06
        • 2013-05-01
        • 1970-01-01
        • 2018-02-06
        • 2011-04-15
        • 1970-01-01
        相关资源
        最近更新 更多