状态 monads/monad 转换器如何在 do 符号中脱糖? 示例说明问题问题

问题描述

示例

sumArray :: Array Int -> State Int Unit
sumArray = traverse_ \n -> modify \sum -> sum + n

t1 :: Int
t1 = execState (do
  sumArray [1,2,3]
  sumArray [4,5]
  sumArray [6]) 0
-- returns 21

module Main where

import Prelude
import Effect (Effect)
import Data.Foldable (fold,traverse_)
import Control.Monad.State (State,execState)
import Control.Monad.State.Class (modify)
import Data.Maybe (Maybe(..))
import Effect.Console (log)

main :: Effect Unit
main = log $ show t1

sumArray :: Array Int -> State Int Unit
sumArray = traverse_ \n -> modify \sum -> sum + n

t1 :: Int
t1 = execState (do
  sumArray [1,5]
  sumArray [6]) 0

{-
execState :: forall s a. State s a -> s -> s
execState (StateT m) s = case m s of Identity (Tuple _ s') -> s'

type State s = StateT s Identity
-}

说明

我如何理解表达式 t1 的求值:

  1. 每个 sumArray 调用都会返回一个状态 monad,其中包含给定的 Int 数组值的总和。
  2. 所有三个状态单子(以某种方式)统一为一个,同时累积中间值。
  3. execState 返回总 Int 和,给定 State Int Unit 和初始值作为输入。

问题

我不能特别理解第 2 步。根据 do notation,像 sumArray [1,3] 这样的表达式会脱糖为 bind x \_ -> ...,因此之前的输入被忽略。如果我用不同的 monad 类型写 do,比如

t2 :: Maybe Int
t2 = do
  Just 3
  Just 4

,编译器抱怨:

Int 类型的结果在 do 符号块中被隐式丢弃。您可以使用 _ <- ... 显式丢弃结果。

,所以规则似乎与 t1 有点不同。

问题

这三个独立的状态单子究竟是如何组合成一个的?更具体地说:为什么运行时计算所有中间状态 monad 总和结果的总体 sum,而不是像 (1+2+3) * (4+5) * 6 这样的东西?换句话说:隐式 + 累加器在哪里?

我觉得我错过了 Chapter 11: Monadic Adventures 的一些概念。

解决方法

@Bergi 的回答已经给出了基本解释 - 但我认为扩展他所说的某些部分并更直接地回答您的一些问题可能会很有用。

根据do notation,像sumArray [1,2,3]这样的表达式 脱糖到 bind x \_ -> ...,所以之前的输入被忽略。

这在某种意义上是完全正确的,但也暴露了一些误解。

一方面,我发现这句话的措辞具有误导性——尽管在原始来源的上下文中它是完全可以接受的。它不是在谈论像 sumArray [1,3] 这样的表达式本身是如何“脱糖”的,而是在谈论如何将 do 块的连续行(“语句”)脱糖成一个“组合”它们的单个表达式- 这似乎是你的整个问题的本质。所以是的,这是真的 - 基本上是 do 符号的定义 - 像

这样的表达式
do a <- x
   y

desugars 到 bind x \a -> y(我们认为 y 是一些更复杂的表达式,大概涉及 a)。同样的

do x
   y

脱糖为 bind x \_ -> y。但后一种情况不是“忽略输入”——它忽略了输出。让我再解释一下。

通常将 m a 类型的通用 monadic 值视为某种“计算”,它“生成”a 类型的值。这必然是一个相当抽象的表述——因为 Monad 是一个如此笼统的概念,一些特定的 Monad 比其他的更适合这种心理图景。但这是理解 monad 基础知识的好方法,特别是 do 符号 - 每一行都可以被认为是某种命令式语言中的“语句”,这可能会产生一些“副作用”(一种受到您正在使用的特定 monad 的严格约束),并且还会产生一个值作为“结果”。

从这个意义上说,上面第一种类型的 do 块——我们使用“左箭头”符号“绑定”结果——正在使用计算值(表示为a) 决定下一步做什么。 (顺便说一句,这就是 monad 与 applicatives 的区别——如果你只有一系列计算并且只想组合它们的“效果”,而不让“中间结果”影响你正在做的事情,你实际上并不需要 monad或 bind。)而第二个不使用第一个计算的结果(该计算为 x) - 这正是我所说的“忽略输出”的意思。它忽略了 x 的结果。这并不(必然)意味着 x 是无用的。它仍然被用于它的“副作用”。

为了使它更具体,我将更详细地查看您的两个示例,从 Maybe monad 中的简单示例开始(我将按照编译器建议的顺序进行更改为了让它开心 - 请注意,我个人对 Haskell 比对 Purescript 更熟悉,所以我可能会弄错这样的 Purescript 特定的东西,因为 Haskell 对您的原始代码完全没问题):

t2 :: Maybe Int
t2 = do
  _ <- Just 3
  Just 4

在这种情况下,t2 将简单地等于 Just 4,并且看起来 - 正确 - do 块的第一行是多余的。但这只是 Maybe 单子如何工作以及我们在那里获得的特定值的结果。我可以很容易地向你证明第一行确实仍然很重要,通过进行此更改

t2 :: Maybe Int
t2 = do
  _ <- Nothing
  Just 4

现在你会发现t2不等于Just 4,不等于Nothing

那是因为 Maybe monad 中的每个“计算”——即每个 Maybe a 类型的值——要么“成功”,要么得到 a 类型的“结果”(由Just 值)或“失败”(由 Nothing 表示)。而且,重要的是,Maybe monad 的定义方式 - 即 bind 的定义 - 故意传播失败。也就是说,在任何时候遇到的任何 Nothing 值都会立即终止计算并返回 Nothing 结果。

所以即使在这里,第一次计算的“副作用”——它成功或失败的事实——确实对整体发生的事情产生重大影响。我们只是忽略“结果”(计算成功时的实际值)。

如果我们现在转向 State monad - 这是一个比 Maybe 更复杂的 monad,但实际上可能因此使上述几点更容易理解。因为这是一个 monad,在那里谈论每个 monadic 值的“副作用”和“结果”确实很有意义 - 在 Maybe 情况下,这可能感觉有点强迫,甚至愚蠢。

State s a 类型的值表示计算结果为 a 类型的值,同时“保持某种状态”为 s 类型。也就是说,计算可以使用当前状态来计算其结果,和/或它可以更新状态作为计算的一部分。具体来说,这与 s -> (a,s) 类型的函数相同 - 它接受一些状态,并返回更新的状态(可能相同)以及计算值。实际上,State s a 类型本质上是这种函数类型的简单 newtype 包装器。

bind 在其 Monad 实例中的实现做了最明显和自然的事情 - 用文字解释比从实际实现细节“看到”要容易得多。通过将原始状态提供给第一个函数,然后从中获取更新的状态并将其提供给第二个函数来组合两个这样的“有状态函数”。 (实际上,bind 需要做 - 而且做 - 比这更多,因为正如我之前提到的,它需要能够使用“结果” - a - 从第一次计算来决定如何处理第二个。但我们现在不需要深入研究,因为在这个例子中我们不使用结果值 - 事实上不能,因为它总是微不足道的 Unit 类型.其实并不复杂,但我不会详细介绍,因为我不想让这个答案更长!)

所以当我们这样做

do
  sumArray [1,3]
  sumArray [4,5]
  sumArray [6]

我们正在构建 State Int Unit 类型的有状态计算 - 即 Int -> (Unit,Int) 类型的函数。由于 Unit 是一个不感兴趣的类型,并且基本上在这里用作“我们不关心任何结果”的占位符,因此我们基本上是从其他三个这样的类型构建 Int -> Int 类型的函数功能。这很容易做到——我们可以组合三个函数!在这个简单的例子中,这就是 bind monad 的 State 实现最终要做的事情。

希望这能回答您的主要问题:

隐式 + 累加器在哪里?

通过表明除了函数组合之外没有“隐式累加器”。事实上,这些单独的函数碰巧(在这种情况下分别)将 6、9 和 6 添加到输入中,导致最终结果是这 3 个数字的总和(因为两个总和的组合是本身是一个和,最终来自加法的结合性)。

但更重要的是,我希望这能让您更全面地了解 Monads 和 do 表示法,您可以将其应用于许多其他情况。

,

每个 sumArray 调用都会返回一个状态 monad,其中包含给定的 Int 数组值的总和。

不,它不返回“状态单子”,也不保存数组的总和。

它返回一个 State Int Unit 值,表示“Unit状态计算”(使用 Int 作为状态)。要获得总和,您实际上必须运行该计算:

t :: State Int Unit
t = sumArray [1,3]

x = runState t 0 // ((),6)
y = runState t 5 // ((),11)

请注意,对于值 y,永远不会计算数组元素的总和 - 它是 1 到 5,然后是 2 到 6,然后是 3 到 8。

这三个独立的状态单子究竟是如何组合成一个的?

理解的关键是它们不是状态值,而是有状态的计算。它们可以通过简单地将这些计算一个接一个地排序,将结果和状态传递给下一个来组合。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...