2

我试图理解 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 部分应用在 bind 的左侧?(Reader r)而不是(Reader r a).
  2. 定义的这一部分发生了(f (r e))什么:,它的目的是什么?

非常感谢你帮助我。

4

3 回答 3

9

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

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

data Foo = Bar | Baz

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

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

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

......*类型在哪里。

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

data Foo a = Bar Integer String | Baz a

再一次,这个定义绑定了三个名称:FooBarBaz。再一次,Foois 在类型命名空间中,Bar并且Baz在 value 命名空间中。然而,它们的类型更复杂:

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只有一个。当您对一个值进行模式匹配时,您正在使用值级构造函数(因为您正在解构使用相同构造函数构建的值),并且该值仅包含一个值(必须如此,因为Reader它是newtype),所以该模式只绑定一个变量。


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

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

为了理解>>=for的定义Reader,让我们专门化>>=to的类型Reader

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

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

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

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

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

让我们考虑一下。我们有两个函数fg,并且我们期望生成一个函数,该函数b从 type 的值产生 type 的值a。我们只有一种方法来产生 a 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类型上,而不是(->)直接定义。这意味着我们需要添加一些Readerwrapper和runReaderunwrapper的用法,否则定义是一样的:

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

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

于 2018-01-21T18:32:33.223 回答
4

要回答您的功能,Monad是 class over types of kind * -> *

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

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

(同样,Writer wis a Monadwhenw是 a MonoidWriteris not a Monad)。


要回答您的第二个问题,更容易考虑Reader e a与 function 相同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
于 2018-01-21T18:27:59.247 回答
1
  1. 没有部分应用。Reader是一个newtype精确地“包装”一个值 ( runReader) 的定义,它是类型的函数e -> a。因此,只需对包装器Reader r外的功能进行模式匹配。绑定到 type 的函数。Readerre -> a

  2. f是一个函数,就像r. 使用 valuer e调用函数,然后使用该函数调用的结果进行调用。ref

于 2018-01-21T18:28:20.360 回答