haskell - 为什么要把Reader的构造函数参数定义为函数呢?

标签 haskell reader-monad

在学习Reader Monad时,我发现它的定义是:

newtype Reader r a = Reader { runReader :: r -> a }

instance Monad (Reader r) where
  return a = Reader $ \_ -> a
  m >>= k  = Reader $ \r -> runReader (k (runReader m r)) r

我想知道为什么使用函数作为构造函数参数而不是其他东西(例如元组):

newtype Reader r a = Reader { runReader :: (r, a) }

instance Monad (Reader r) where
  -- Here I cannot get r when defining return function, 
  -- so does that's the reason that must using a function whose input is an "r"?
  return a = Reader (r_unknown, a) 
  m >>= k = Reader (fst $ runReader m) (f (snd $ runReader m))

根据Reader的定义,我们需要一个可以用来生成“值”的“环境”。我认为 Reader 类型应该包含“环境”和“值”的信息,所以这个元组看起来很完美。

最佳答案

您在问题中没有提到它,但我猜您特别想到使用一对来定义 Reader 因为将其视为提供固定环境的一种方式也是有意义的。假设我们在 Reader monad 中有一个较早的结果:

return 2 :: Reader Integer Integer

我们可以使用这个结果在固定环境中进行进一步的计算(并且 Monad 方法保证它在整个 (>>=) 链中保持固定):

GHCi> runReader (return 2 >>= \x -> Reader (\r -> x + r)) 3
5

(如果您替换上面表达式中的 return(>>=)runReader 的定义并简化它,您将确切地了解它如何减少为 2 + 3。)

现在,让我们按照您的建议进行定义:

newtype Env r a = Env { runEnv :: (r, a) }

如果我们有一个 r 类型的环境和一个 a 类型的先前结果,我们可以用它们创建一个 Env r a 。 ..

Env (3, 2) :: Env Integer Integer

...我们还可以从中得到新的结果:

GHCi> (\(r, x) -> x + r) . runEnv $ Env (3, 2)
5

那么,问题是我们是否可以通过 Monad 接口(interface)捕获这种模式。答案是不。虽然成对的 Monad 实例,但它的作用完全不同:

newtype Writer r a = Writer { Writer :: (r, a) }

instance Monoid r => Monad (Writer r) where
    return x = (mempty, x)
    m >>= f = Writer 
        . (\(r, x) -> (\(s, y) -> (mappend r s, y)) $ f x)
        $ runWriter m

需要 Monoid 约束,以便我们可以使用 mempty (这解决了您注意到的必须创建 r_unknown 的问题的无处)和 mappend (这使得可以以不违反 monad 定律的方式组合该对的第一个元素)。然而,这个 Monad 实例所做的事情与 Reader 实例非常不同。该对的第一个元素不是固定的(它可能会发生变化,因为我们将其他生成的值映射到它)并且我们不使用它来计算该对的第二个元素(在根据上面的定义,y 既不依赖于 r 也不依赖于 s)。 Writer 是一个记录器;这里的r值是输出,而不是输入。

<小时/>

但是,有一种方法可以证明您的直觉:我们无法使用一对来创建类似阅读器的单子(monad),但我们可以创建类似阅读器的co单子(monad)。非常宽松地说,Comonad是将 Monad 界面颠倒过来后得到的结果:

-- This is slightly different than what you'll find in Control.Comonad,
-- but it boils down to the same thing.
class Comonad w where
    extract :: w a -> a                 -- compare with return
    (=>>) :: w a -> (w a -> b) -> w b   -- compare with (>>=)

我们可以给我们放弃的 Env 一个 Comonad 实例:

newtype Env r a = Env { runEnv :: (r, a) }

instance Comonad (Env r) where
    extract (Env (_, x)) = x
    w@(Env (r, _)) =>> f = Env (r, f w)

这使我们能够从头开始用 (=>>) 编写 2 + 3 示例:

GHCi> runEnv $ Env (3, 2) =>> ((\(r, x) -> x + r) . runEnv) 
(3,5)

了解其工作原理的一种方法是注意 a -> Reader r b 函数(即您为 Reader(>>= )) 本质上与 Env r a -> b 1 相同(即您赋予 Env(=>>) ):

a -> Reader r b
a -> (r -> b)     -- Unwrap the Reader result
r -> (a -> b)     -- Flip the function
(r, a) -> b       -- Uncurry the function
Env r a -> b      -- Wrap the argument pair

作为这一点的进一步证据,这里有一个将一个函数更改为另一个函数的函数:

GHCi> :t \f -> \w -> (\(r, x) -> runReader (f x) r) $ runEnv w
\f -> \w -> (\(r, x) -> runReader (f x) r) $ runEnv w
  :: (t -> Reader r a) -> Env r t -> a
GHCi> -- Or, equivalently:
GHCi> :t \f -> uncurry (flip (runReader . f)) . runEnv
\f -> uncurry (flip (runReader . f)) . runEnv
  :: (a -> Reader r c) -> Env r a -> c

为了总结,这里有一个稍微长一点的示例,其中并排有 ReaderEnv 版本:

GHCi> :{
GHCi| flip runReader 3 $
GHCi|     return 2 >>= \x ->
GHCi|     Reader (\r -> x ^ r) >>= \y ->
GHCi|     Reader (\r -> y - r)
GHCi| :}
5
GHCi> :{
GHCi| extract $
GHCi|     Env (3, 2) =>> (\w ->
GHCi|     (\(r, x) -> x ^ r) $ runEnv w) =>> (\z ->
GHCi|     (\(r, x) -> x - r) $ runEnv z)
GHCi| :}
5

关于haskell - 为什么要把Reader的构造函数参数定义为函数呢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42310931/

相关文章:

haskell - 有用于文本的 Haskell readMaybe 函数吗?

Haskell 非法派生项泛型

java - 如何从 Java 使用 scalaz.Reader

haskell - 再来一次……我可以举一个状态单子(monad)的例子吗?

Scalaz: `scalaz.syntax.applicative._` 如何发挥它的魔力

clojure - Clojure 中的 reader monad 和偏函数有什么区别?

haskell - Haskell 中的函数类型特化

haskell - 在这种情况下,UNPACK 编译指示做什么?

haskell - Functor 实例 for (newtype Mu f = InF {outF::f (Mu f)})

scala - Scala : return, 本地的 Reader monad 和序列