首页 > 解决方案 > 状态单子/单子转换器如何在 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, 2, 3]
  sumArray [4, 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调用都返回一个状态单子,其中包含给定Int数组值的总和。
  2. 所有三个状态单子都(以某种方式)统一为一个单子,同时累积中间和。
  3. execState返回Int总和、给定State Int Unit值和初始值作为输入。

问题

我特别不能完全理解第 2 步。根据do notationsumArray [1, 2, 3] ,类似deugars to的表达式bind x \_ -> ...,因此之前的输入被忽略。如果我do用不同的单子类型写

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

,编译器抱怨:

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

,所以规则似乎与t1.

问题

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

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

标签: functional-programmingmonadsmonad-transformerspurescriptdo-notation

解决方案


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

根据do notationsumArray [1, 2, 3] ,类似 deugars to的表达式bind x \_ -> ...,因此之前的输入被忽略。

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

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

do a <- x
   y

deugars to bind x \a -> y(我们认为这y是一些更复杂的表达式,可能涉及a)。同样,

do x
   y

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

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

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

为了使其更具体,我将更详细地查看您的两个示例,从Maybemonad 中的简单示例开始(我将进行编译器建议的更改以使其满意 - 请注意,我'我个人对 Haskell 比对 Purescript 更熟悉,所以我可能会得到像这样错误的 Purescript 特定的东西,因为 Haskell 对你的原始代码完全没问题):

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

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

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

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

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

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

如果我们现在转到Statemonad - 这是一个比 更复杂的 monad Maybe,但实际上可能由于这个原因使上述几点更容易理解。因为这是一个单子,在这里谈论每个单子值的“副作用”和“结果”确实很有意义——在这种Maybe情况下,这可能让人觉得有点强迫,甚至是愚蠢的。

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

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

所以当我们做

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

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

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

隐式+累加器在哪里?

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

但更重要的是,我希望这能让你对 Monads 和do符号有更全面的解释,你可以将其应用于许多其他情况。


推荐阅读