【问题标题】:Reader Monad clarification读者单子澄清
【发布时间】:2018-06-30 09:38:45
【问题描述】:

我试图理解 reader monad,但似乎无法理解 bind (>>=) 在这个 monad 中的作用。

这是我正在分析的实现:

newtype Reader e a = Reader { runReader :: (e -> a) }

instance Monad (Reader e) where 
    return a         = Reader $ \e -> a 
    (Reader r) >>= f = Reader $ \e -> runReader (f (r e)) e
  1. 我的第一个问题是,为什么 Reader 部分应用在左侧 绑定的手边? (Reader r) 而不是 (Reader r a)
  2. 定义的这一部分发生了什么:(f (r e)),它的目的是什么?

非常感谢您帮助我。

【问题讨论】:

    标签: haskell monads reader-monad


    【解决方案1】:

    我的第一个问题是,为什么 Reader 部分应用在 bind 的左侧? (Reader r) 而不是 (Reader r a)

    不是。 Reader 的使用已经完全饱和,因为它必须如此。但是,我可以理解您的困惑……请记住,在 Haskell 中,类型和值位于不同的命名空间中,使用 datanewtype 定义数据类型会将新名称带入两个命名空间的范围内。例如,考虑以下声明:

    data Foo = Bar | Baz
    

    此定义绑定了三个名称,FooBarBaz。但是,等号左侧的部分绑定在类型命名空间中,因为Foo 是一个类型,而右侧的构造函数绑定在值命名空间中,因为Bar 和@ 987654332@ 本质上是值。

    所有这些东西也都有类型,这有助于可视化。 Foo 有一个kind,本质上是“类型级别事物的类型”,BarBaz 都有一个类型。这些类型可以写成如下:

    Foo :: *
    Bar :: Foo
    Baz :: Foo
    

    * 是类型的种类。

    现在,考虑一个稍微复杂一点的定义:

    data Foo a = Bar Integer String | Baz a
    

    再一次,此定义绑定了三个名称:FooBarBaz。同样,Foo 在类型命名空间中,BarBaz 在值命名空间中。然而,它们的类型更复杂:

    Foo :: * -> *
    Bar :: Integer -> String -> Foo a
    Baz :: a -> Foo a
    

    这里,Foo 是一个类型构造函数,所以它本质上是一个接受类型 (*) 作为参数的类型级函数。同时,BarBaz 是接受各种值作为参数的值级函数。

    现在,回到Reader 的定义。暂时避免记录语法,我们可以将其重新编写如下:

    newtype Reader r a = Reader (r -> a)
    

    这会在类型命名空间中绑定一个名称,在值命名空间中绑定一个名称,但令人困惑的是它们都被命名为Reader!不过,这在 Haskell 中是完全允许的,因为命名空间是分开的。在这种情况下,每个Reader 也有一个种类/类型:

    Reader :: * -> * -> *
    Reader :: (r -> a) -> Reader r a
    

    请注意,类型级别 Reader 接受两个参数,但值级别 Reader 只接受一个。当您对一个值进行模式匹配时,您正在使用值级构造函数(因为您正在解构使用相同构造函数构建的值),并且该值仅包含一个值(必须如此,因为 @987654357 @ 是 newtype),所以该模式只绑定一个变量。


    这部分定义发生了什么:(f (r e)),它的目的是什么?

    Reader 本质上是一种组合许多函数的机制,这些函数都采用相同的参数。这是一种避免在任何地方都使用线程的方法,因为各种实例会自动进行管道连接。

    要了解>>=Reader 的定义,让我们将>>= 的类型特化为Reader

    (>>=) :: Reader r a -> (a -> Reader r b) -> Reader r b
    

    为了清楚起见,我们还可以将Reader r a 扩展为r -> a,以便更好地了解类型的实际含义

    (>>=) :: (r -> a) -> (a -> r -> b) -> (r -> b)
    

    为了讨论的目的,我们也在这里命名参数:

    (>>=) :: (r -> a) -> (a -> r -> b) -> (r -> b)
    (>>=)    f           g             =  ...
    

    让我们考虑一下。我们有两个函数,fg,我们期望生成一个函数,该函数从 a 类型的值中生成 b 类型的值。我们只有一种方法来产生b,那就是调用g。但是为了调用g,我们必须有一个a,而我们只有一种方法可以得到a:调用f!我们可以调用f,因为它只需要一个我们已经拥有的r,因此我们可以开始将这些函数附加在一起以生成我们需要的b

    这有点令人困惑,因此直观地查看这种值流可能会有所帮助:

              +------------+
              | input :: r |
              +------------+
                 |       |
                 v       |
    +--------------+     |
    | f input :: a |     |
    +--------------+     |
           |             |
           v             v
      +------------------------+
      | g (f input) input :: b |
      +------------------------+
    

    在 Haskell 中,如下所示:

    f >>= g = \input -> g (f input) input
    

    ...或者,稍微重命名以匹配您问题中的定义:

    r >>= f = \e -> f (r e) e
    

    现在,我们需要重新引入一些包装和展开,因为真正的定义是在 Reader 类型上,而不是直接在 (->) 上。这意味着我们需要添加Reader wrapper 和runReader unwrapper 的一些用法,否则定义相同:

    Reader r >>= f = Reader (\e -> runReader (f (r e)) e)
    

    此时,您可以检查一下您的直觉:Reader 是一种在许多函数之间传递值的方法,这里我们组合了两个函数,rf。因此,我们需要将值传递两次,我们这样做:在上面的定义中e 有两种用法。

    【讨论】:

      【解决方案2】:

      为了回答你的功能,Monadkind * -> * 类型的类。

      • [Int] :: * = 整数列表
      • [] :: * -> * = 列表
      • Reader e a :: * = 环境为 e 的阅读器导致
      • Reader e :: * -> * = 环境为 e 的读者
      • Reader :: * -> * -> * = 读者

      当我们说 Reader 有一个 monad 实例时,我们很糟糕,当我们的意思是 Reader 与任何环境都有一个 monad 实例

      (同样,Writer wMonad,而 wMonoidWriter 不是 Monad)。


      要回答您的第二个问题,从以下方面考虑更容易 Reader e a 与函数e -> a 相同。

      函数具有相同的 monad 定义,但没有 newtype 包装器和 解包器。想想Reader = (->)

      instance Monad ((->) e) where
          return x = \_ -> x -- alternatively, return = const !
          r >>= f  = \e -> f (r e) e
      

      然而,join 的定义可能是最有见地的:

      join :: Reader e (Reader e a) -> Reader e a
      

      但只有功能:

      join :: (e -> e -> a) -> (e -> a)
      join f e = f e e
      

      如果我们为Reader 编写它,我们必须在其中添加runReaderReader 正确的位置(并将变量绑定移动到 RHS 上的 lambda):

      join f = Reader $ \e -> runReader (runReader f e) e
      

      【讨论】:

        【解决方案3】:
        1. 没有部分应用。 Reader 是一个 newtype 定义,它精确地“包装”了 one 值 (runReader),它是 e -> a 类型的函数。所以,Reader r 只是模式匹配 Reader 包装器之外的函数。 r 绑定到 e -> a 类型的函数。

        2. f 是一个函数,r 也是一个函数。 r e 使用值 e 调用函数 r,然后使用该函数调用的结果调用 f

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-08-05
          • 2011-06-09
          • 1970-01-01
          • 1970-01-01
          • 2019-11-15
          • 2013-06-05
          相关资源
          最近更新 更多