读者莫纳德澄清



我试图理解读者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 r)而不是(Reader r a).
  2. 这部分定义是怎么回事:(f (r e)),它的目的是什么?

非常感谢你帮助我。

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

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

data Foo = Bar | Baz

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

所有这些东西也有类型,这有助于可视化。Foo有一个类型,本质上是"类型级事物的类型",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只接受一个参数。当您对值进行模式匹配时,您使用的是值级构造函数(因为您正在解构使用同一构造函数构建的值),并且该值仅包含一个值(因为它必须,因为Reader是一个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包装器和runReader解包器的用法,但除此之外,定义是相同的:

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

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

为了回答你的函数,Monad类是类于* -> *类型。

  • [Int] :: *= 整数列表
  • [] :: * -> *= 列表
  • Reader e a :: *= 环境 e 的读取器导致
  • Reader e :: * -> *= 具有环境 e 的读取器
  • Reader :: * -> * -> *= 读取器

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

(类似地,当wMonoid时,Writer w是一个MonadWriter不是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
  1. 没有部分应用程序。Reader是一个newtype定义,它只"包装">一个值(runReader),它是类型e -> a的函数。因此,Reader r只是模式匹配Reader包装器中的函数。r绑定到类型e -> a的函数。

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

相关内容

  • 没有找到相关文章

最新更新