【问题标题】:Understanding how reader monad simulates global variables了解 reader monad 如何模拟全局变量
【发布时间】:2020-06-24 03:52:28
【问题描述】:

据我所知,monad 尝试模拟全局变量的方式是将变量(或输入)“存储”在函数中(并且函数也是变量),并将函数存储到另一个函数中通过“通过它”;最后,链中的所有函数都成为一个函数,可以折叠多个较小的函数。

例子:

(+2) >>= \a-> (+3) >>= \b -> return(a+b)

根据教程,>>= 的工作方式类似于 f >>= g = \w -> g (f w) w 所以我的理解是我们得到了一个接受两个输入的函数,一个w(变量)和一个函数(也使用该变量) 并输出((+3) w)。在这里,w 是输入。

所以这一行:

(+2) >>= \a-> (+3) >>= \b -> return(a+b)

等价于

 \w -> (\(2+w) -> (+3)  w) >>=\b -> return (a+b)
                                 a

(并且这个2+w函数被“存储”在(\a->(+3))的第一个输入\a中)。

同样:

\w2 -> (\b ->return (...))  ( \w -> (\(2+w) -> (+3)  w) ) w2

\b存储在\w -> (\(2+w) -> (+3) w)中:

\w2 -> (  \(( \w -> (\(2+w) -> (+3)  w) ) -> return(...)) w2

那么,return (a+b) 实际上不仅仅是简单地做return x = \_ -> x,而且还能够从\w2 -> ( \b(a inside) -> return(...)) w2 的大函数中取出函数(ab)来获得另一个大函数:

( (\w -> 2+w) +(\w -> (\(2+w) -> (+3)  w) )

我理解正确吗?还是我完全错了。我没有找到任何关于 return 的教程(从函数中获取输入)。单子对我来说仍然很困惑。

【问题讨论】:

  • 您是否意识到您描述的是函数的 Monad 实例的工作方式,而不是一般 monad 的工作方式,并且其他 monad 的工作方式都不同?
  • 不要通过一个例子来学习单子。有一个笑话说“一旦你了解了 monad 是什么,就失去了解释它的能力”。这样的笑话隐藏了一个事实:monad 一个抽象概念,查看 monad 实例的一个示例将揭示这种抽象的一小部分。想象一下,你想了解一个类是什么,只需查看它的一个实例……对于单子来说也是一样的。阅读有关MaybeReaderState monads 的所有内容,并尝试找出它们共享的共同模式,然后继续研究其他模式。我建议周一早上的 haskell 博客作为初学者材料。
  • Monads: Kleisli Arrows compose 关联,return 是该组合的标识。
  • 您的帖子格式非常糟糕,以至于几乎无法理解。通常,一次问一个问题。发布许多小而有针对性的问题是可以的,实际上是好的和适当的。您始终可以包含指向您之前相关帖子的链接。你可以从编辑这个开始。 :) 如果你这样做,请@ping 我。
  • 如果您想了解 monad 的概念,请研究任何特定 monad 的实现如何遵守 monad 法律。这将帮助您了解简单术语级效果隐藏的不同 monad 之间的相似之处。

标签: haskell monads


【解决方案1】:

让我们以两种方式一步一步地查看您的示例。首先,让我们看看 do 符号等价物是什么:

do
  a <- (+ 2)
  b <- (+ 3)
  return (a + b)

这本质上是对以下重复模式的抽象,其中许多不同的函数都应用于相同的共享“环境”值,然后返回一些最终结果:

\ env -> let
  a = (+ 2) env
  b = (+ 3) env
  in (a + b)

实际上,为了与其他动作保持一致,return 也接收环境,而只是忽略它:

\ env -> let
  a = (+ 2) env
  b = (+ 3) env
  in const (a + b) env  -- const x y = x

我们可以通过让&gt;&gt;= 携带此环境并将其传递给每个函数((+ 2)(+ 3)const (a + b))来使其适合Monad 模式。

&gt;&gt;= 有两个函数functioncontinue,其中function 是上面的(+ 2)(+ 3) 之一; &gt;&gt;=function 应用于环境,然后将两件事传递给continuefunction 的结果,以及再次环境,以便continue 中的后续步骤也可以读取环境。

-- Type:
(>>=) :: (Monad m) => m        a -> (a -> m        b) -> m        b
-- or:
(>>=) ::              (->) env a -> (a -> (->) env b) -> (->) env b
-- or:
(>>=) ::              (env -> a) -> (a -> env ->   b) -> env ->   b

-- Implementation:
instance Monad ((->) env) where

  function >>= continue = \ env -> continue (function env) env
  -- or:
  (>>=) = \ function continue env -> continue (function env) env

事实上,使用{-# LANGUAGE InstanceSigs #-} 扩展,您可以将上面的最后一个类型准确地写为&gt;&gt;=Monad 实例中的签名,我建议使用该扩展来帮助您实现类型类学习。

return 接受一个值x,并生成一个函数来代替上面的continue,正如我之前提到的,它只是忽略环境并返回x

return x = \ _env -> x
-- =
return = \ x _env -> x
-- =
return = const

其次,如果我们回到去糖的do 表示法,并添加括号以阐明 lambda 的范围:

(+ 2) >>= (\ a -> (+ 3) >>= (\ b -> return (a + b)))

然后我们可以内联&gt;&gt;=return 的定义,看看它们在这种情况下是如何工作的。我已经对每个替换中的变量进行了编号,以使范围更加明确。

(\ function1 continue1 env1 -> continue1 (function1 env1) env1)
  (+ 2)
  (\ a ->
    (\ function2 continue2 env2 -> continue2 (function2 env2) env2)
      (+ 3)
      (\ b ->
        (\ x env3 -> x)
          (a + b)))

然后我们可以通过将函数应用到它们的参数来替换所有变量(beta-reduction):

let
  function1 = (+ 2)
  continue1 = \ a ->
    let
      function2 = (+ 3)
      continue2 = \ b ->
        let
          x = (a + b)
        in \ _env3 -> x

    in \ env2 -> continue2 (function2 env2) env2

in \ env1 -> continue1 (function1 env1) env1)

-- =

\ env1 ->
  (\ a env2 ->
    (\ b _env3 -> a + b)
      (env2 + 3)
      env2)
    (env1 + 2)
    env1

-- =

\ env1 -> let
  a = (env1 + 2)
  env2 = env1
  in let
    b = (env2 + 3)
    _env3 = env2
    in (a + b)

那么我们可以省略所有多余的env变量,因为它们都是相等的,只是用来将值传递给所有函数:

\ env -> let
  a = (env + 2)
  b = (env + 3)
  in (a + b)

这正是我们试图抽象出来的代码!

不过,这只是一个Monad 实例;所有其他实例的工作方式根据所涉及的特定类型而有所不同。所有基本实例的共同点是它们在纯代码中抽象了一些常见的重复模式,例如:

-- State:

\ state1 -> let
  (a, state2) = function1 state1
  (b, state3) = function2 state2
  (c, state4) = function3 state3
  …
  in (x, stateX)

-- Writer:

\ log1 -> let
  (a, log2) = function1
  (b, log3) = function2
  (c, log4) = function3
  …
  in (x, log1 <> log2 <> log3 <> log4)

-- Maybe:

case function1 of
  Just a -> case function2 of
    Just b -> case function3 of
      Just c -> …
          Just x -> Just x
          Nothing -> Nothing
      Nothing -> Nothing
    Nothing -> Nothing
  Nothing -> Nothing

-- Either:

case function1 of
  Right a -> case function2 of
    Right b -> case function3 of
      Right c -> …
          Right x -> Right x
          Left errorX -> Left errorX
      Left error3 -> Left error3
    Left error2 -> Left error2
  Left error1 -> Left error1

-- List:

concatMap
  (\ a -> concatMap
    (\ b -> concatMap
      (\ c -> concatMap
          …
         [x])
      function3)
    function2)
  function1

同样,基本的 monad transformers 抽象了 monadic 代码中的这些模式。

-- MaybeT:

do
  ma <- function1
  case ma of
    Just a -> do
      mb <- function2
      case mb of
        Just b -> do
          mc <- function3
          case mc of
            Just c -> …
                Just x -> pure (Just x)
                Nothing -> pure Nothing
            Nothing -> pure Nothing
        Nothing -> pure Nothing
      Nothing -> pure Nothing
    Nothing -> pure Nothing

Monad/Applicative/Functor 层次结构为数据结构和控制结构提供了一种通用方法,可以抽象出像这样的某种顺序模式,因此它们可以全部do 符号和库函数,如 replicateM,适用于 any Monad

查看很多个不同的例子,而不仅仅是一个例子,有助于了解它们的共同点和不同之处。

【讨论】:

  • 很好的解释。为什么我们首先需要env 作为参数?函数自然是全局的,因为它们是纯粹的。常数值也是纯的。函数是纯值。依赖全局常量的风险是什么?无论如何,我知道这是题外话,不适合发表评论。我应该提出一个问题..
  • @bob 这只是一个抽象,我们用它来做的打字更少(所以出错的机会也更少)。 do { x &lt;- (+1) ; y &lt;- (*2) ; return (x+y) } 比对应的 let 短。与状态传递或非值处理类似。我们不需要 monad,我们可以显式地输入所有冗长的代码。 Lisp 有宏,Haskell 有更原则的单子方法。 --- 还有 MonadComprehensions 扩展,例如[ x+y | x &lt;- (+1), y &lt;- (*2) ],越来越接近伪代码 for 理解、{ x+y FOR x FROM (+1) AND y FROM (*2) } 等。
  • @bob 一如既往,Haskell 的命名已关闭。 do 应该被命名为 forreturn - yield
  • @bob:如果您想使用local 对环境进行本地修改,它会变得更加方便。 Reader/ReaderT 允许环境是动态的,而全局变量必须是静态的。如果您已经有IO 可用,使用data MutEnv = MutEnv { envMutableStuff :: IORef Stuff }ReaderT MutEnv IO 比使用纯data Env = Env { envStuff :: Stuff }StateT Env IO 更有效,并且全局IORef 需要不安全的代码。
【解决方案2】:

在您的问题中,您正在描述一个特定的单子,即“读者”单子。在您试图理解的细节级别上,这个 reader monad 的行为与其他 monad 不同。

也就是说,我认为您对 reader monad 的详细操作的理解是部分正确的。但是,您错过了几件事:您弄乱了示例表达式中的精确操作顺序,该顺序由 lambda 表达式的解析方式决定,并且您在某种程度上掩盖了结合了\w -&gt; ... lambdas 由每个 &gt;&gt;= 运算符引入,因此它们都引用同一个全局变量。

为了更清楚一点,让我详细说明一下,使用与 GHC 实际评估您的代码的方式相匹配的表达式的简单转换...

让我们以你的为例:

(+2) >>= \a -> (+3) >>= \b -> return (a+b)

并从插入一些括号开始。尽管运算符 &gt;&gt;= 是左关联运算符,但像 \a -&gt; ... 这样的 lambda 表达式会尽可能多地吸收有效表达式,因此该表达式中括号的正确位置是:

(+2) >>= ( \a -> ( (+3) >>= ( \b -> return (a+b) ) ) )         -- (1)

现在,正如您所注意到的,&gt;&gt;= 的定义是:

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

另外,return 的定义是:

return x = \w -> x

让我们来处理 (1) 中最右边出现的 &gt;&gt;= 的表达式:

(+3) >>= ( \b -> return (a+b) )
=   -- apply definition of `>>=` with f = (+3); g = \b -> return (a+b)
\w -> (\b -> return (a+b)) ((+3) w) w
=   -- apply lambda expression (\b -> ...) to first argument ((+3) w)
\w -> (return (a + (+3) w)) w
=   -- apply definition of `return` (with renamed variable w')
\w -> (\w' -> a + (+3) w) w
=   -- apply lambda expression (\w' -> ...) to first argument w
\w -> a + (+3) w

所以,我们有:

(+3) >>= ( \b -> return (a+b) )    ===    \w -> a + (+3) w     -- (2)

并且,在意义上,运算符&gt;&gt;= 通过将函数(+3) 作为参数b 传递来工作。但是,它并没有真正做到这一点,因为参数b 具有数字类型,而不是函数类型。相反,它为“全局变量”\w -&gt; ...“拉出”一个 lambda,然后将 (+3) w 作为b 传递,这可以正常工作,因为(+3) w 具有数字类型。

现在,让我们看看原始示例 (1) 与替换 (2):

-- from (1) and the subsitution (2):
(+2) >>= ( \a -> ( \w -> a + (+3) w ) )
=   -- apply defn of >>= with f = (+2); g = \a -> (\w -> a + (+3) w)
\w' -> (\a -> (\w -> a + (+3) w)) ((+2) w') w'
=   -- apply lambda (\a -> ...) to ((+2) w')
\w' -> (\w -> (+2) w' + (+3) w) w'
=   -- apply lambda (\w -> ...) to w'
\w' -> (+2) w' + (+3) w'

同样,在意义上,运算符&gt;&gt;= 的工作原理是将函数(+2) 作为参数a 传递。由于它不能从字面上做到这一点,它通过为“全局变量”\w' -&gt; ... 拉出一个 lambda 并将(+2) w' 作为a 传递,然后它有点“合并”\w -&gt; ... lambda 拉在最右边的 &gt;&gt;= 运算符旁边加上 \w' -&gt; ...,因此它们指向同一个全局变量。

显然,如果我们在左侧添加额外的类似绑定:

(+8) >>= \e -> (+1) >>= \o -> (+2) >>= \a -> (+3) >>= \b -> return (e+o+a+b)

我们会得到相同的模式,即为全局变量添加一个新的 lambda \w' -&gt; ...,然后将其与现有的 lambda \w -&gt; ...“合并”:

\w -> (+8) w + (+1) w + (+2) w + (+3) w

【讨论】:

    猜你喜欢
    • 2016-11-15
    • 1970-01-01
    • 2018-03-26
    • 1970-01-01
    • 2020-10-09
    • 2016-04-26
    • 2019-06-10
    • 1970-01-01
    相关资源
    最近更新 更多