读者Monad澄清

osk*_*132 2 monads haskell reader-monad

我试图理解读者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
Run Code Online (Sandbox Code Playgroud)
  1. 我的第一个问题是,为什么Reader部分应用于绑定的左侧?(Reader r)而不是(Reader r a).
  2. 定义的这一部分发生了(f (r e))什么:它的目的是什么?

非常感谢帮助我.

Ale*_*ing 8

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

事实并非如此.使用Reader完全饱和,必须如此.但是,我可以理解你的困惑......请记住,在Haskell中,类型和值位于不同的名称空间中,并在两个名称空间中定义数据类型datanewtype将新名称带入范围.例如,请考虑以下声明:

data Foo = Bar | Baz
Run Code Online (Sandbox Code Playgroud)

这个定义结合了三个名字,Foo,Bar,和Baz.然而,在等号的左手侧部分符号是结合在类型名称空间中,由于Foo是一种类型,并且在右手侧上的构造结合在值的命名空间,因为BarBaz基本上值.

所有这些东西都有类型,这有助于可视化.Foo一种,它在本质上是"类型的类型层次的东西",并BarBaz双方都有一个类型.这些类型可以写成如下:

Foo :: *
Bar :: Foo
Baz :: Foo
Run Code Online (Sandbox Code Playgroud)

...... *那种类型.

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

data Foo a = Bar Integer String | Baz a
Run Code Online (Sandbox Code Playgroud)

再次,这个定义结合了三个名字:Foo,Bar,和Baz.再次,Foo在类型命名空间中,Bar并且Baz在值命名空间中.然而,它们的类型更精细:

Foo :: * -> *
Bar :: Integer -> String -> Foo a
Baz :: a -> Foo a
Run Code Online (Sandbox Code Playgroud)

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

现在,回到定义Reader.暂时避免使用记录语法,我们可以按如下方式重新表述:

newtype Reader r a = Reader (r -> a)
Run Code Online (Sandbox Code Playgroud)

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

Reader :: * -> * -> *
Reader :: (r -> a) -> Reader r a
Run Code Online (Sandbox Code Playgroud)

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


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

Reader本质上是一种组成许多函数的机制,这些函数都采用相同的参数.这是一种避免必须在任何地方处理值的方法,因为各种实例将自动执行管道.

要理解的定义>>=Reader,我们专门的类型>>=Reader:

(>>=) :: Reader r a -> (a -> Reader r b) -> Reader r b
Run Code Online (Sandbox Code Playgroud)

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

(>>=) :: (r -> a) -> (a -> r -> b) -> (r -> b)
Run Code Online (Sandbox Code Playgroud)

为了讨论,让我们在这里命名参数:

(>>=) :: (r -> a) -> (a -> r -> b) -> (r -> b)
(>>=)    f           g             =  ...
Run Code Online (Sandbox Code Playgroud)

让我们暂时考虑一下.我们给出了两个函数,f并且g,我们期望生成一个函数,该函数b从类型的值生成类型的值a.我们只有一种方法可以产生一个b,这就是通过调用g.但是为了打电话g,我们必须有一个a,我们只有一种方式来获得a:召唤f!我们可以调用f,因为它只需要一个r我们已经拥有的,所以我们可以开始将这些函数连接在一起以产生b我们需要的东西.

这有点令人困惑,因此可以直观地看到这种价值流:

          +------------+
          | input :: r |
          +------------+
             |       |
             v       |
+--------------+     |
| f input :: a |     |
+--------------+     |
       |             |
       v             v
  +------------------------+
  | g (f input) input :: b |
  +------------------------+
Run Code Online (Sandbox Code Playgroud)

在Haskell中,这看起来像这样:

f >>= g = \input -> g (f input) input
Run Code Online (Sandbox Code Playgroud)

...或者,重新命名一些东西以匹配您问题中的定义:

r >>= f = \e -> f (r e) e
Run Code Online (Sandbox Code Playgroud)

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

Reader r >>= f = Reader (\e -> runReader (f (r e)) e)
Run Code Online (Sandbox Code Playgroud)

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