【问题标题】:continuations as meaningful comprehensions延续作为有意义的理解
【发布时间】:2015-11-15 16:11:48
【问题描述】:

Monad 可以理解为容器的形式:

  • 列表:给定类型的项目聚合
  • 包:无序聚合
  • set : 忽略多重性的无序聚合
  • 可能:最多聚合一项
  • 阅读器 e:基数聚合 |e|

我想知道如何以一种有意义的方式将延续在此视图中解释为包/容器的形式。谢谢!

【问题讨论】:

  • 继续这种直觉的正确方法可能是放弃它:并非所有 monad 都是容器(或者至少不是很优雅)。例如,StateWriter 和 - 是的 - Cont 根本不像容器。 “容器”是一种直觉,可能会帮助你踏上第一步,但它是你需要概括的东西。 (顺便说一句,对我来说,“容器”对函子来说是一种更有用的直觉——但同样,无论如何也不完美。)
  • 你为什么要寻找“直觉”而不是理解?如果 monad 是直观的,那么您就已经理解了它们,而无需提出任何问题!作为容器的 Monads 更像是一个类比:一个适用于 List 和 Maybe 的类比,有点适用于 IO,而对于大多数其他人来说则相当紧张。
  • @Moses:我唯一推荐的 monad 教程是 “You Could Have Invented Monads! (And Maybe You Already Have.)”,作者是 Dan “sigfpe” Piponi。与其说是关于直觉,不如说是关于为什么我们需要 monad:monad 让我们能够组合函数来做一些other的事情,而不仅仅是产生结果。 (但实际上,这是一个苍白的一句话总结——整篇文章都值得一读。)
  • 我不是那个说;的人;你正在寻找@chi。既然已经提出这个话题,我最喜欢的 monad 文章是 Why Do Monads Matter。我知道它不会是每个人的最爱,因为你必须find your own burrito,但也许它会一路帮助你。

标签: haskell monads continuations


【解决方案1】:

理解 monad 的真正关键是不要试图说出来

单子是 X

然后开始说

X 是一个单子

如果它是has a certain structure and obeys certain laws,那么它就是一个单子。出于在 Haskell 中编程的目的,如果它具有正确的种类和类型并遵守 Monad laws,那么它就是 Monad

 return a >>= f ≡ f a
 m >>= return   ≡ m
(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)

Gabriel Gonzales 指出the monad laws are "the category laws in disguise"。我们可以使用>=>,其定义如下,而不是>>=

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

当我们这样做时,monad 法则变成了 category laws 与身份 return 和关联组合 >=>

return >=> f    ≡ f
f >=> return    ≡ f
f >=> (g >=> h) ≡ (f >=> g) >=> h

在您的示例中,您已经讨论了 monad 要成为的两种不同的东西:扁平化的聚合和修剪的聚合。与其说 monad 是这两种东西,不如说这两种东西都是 monad。为了培养对什么是 monad 的直觉,让我们来谈谈最大的一类东西,它们都是 monad。

嫁接的树木

Monads 的最大类是 trees with grafting 和简化。这个类是如此之大,以至于我在 Haskell 中所知道的每个 Monad 都是一棵嫁接的树。这些 monad 中的每一个都包含一个 return,它构造一棵在叶子上保存值的树,以及一个绑定 >>=,它做两件事。 Bind 用一棵新树替换每片叶子,将新树嫁接到叶子所在的树上,并简化生成的树。您提到的示例在简化树的方式上有所不同。允许哪些简化受Monad 法律的约束。

如果我们有一棵简单的树在它的叶子上保存值,它就是一个 monad。

data Tree a = Branch [Tree a] | Leaf a

它的return 构造一个Leaf。这里是return 1

          1

它的绑定用整棵树替换叶子。我们从以下树开始:

          *
         / \
        0   1

并将其绑定到\x -> Branch [x*2, x*2 + 1]。我们用新计算的树替换每个叶子。

        __*__
       /     \
      *       *
     / \     / \
    0   1   2   3

对于普通树木,嫁接步骤不会进行任何简化。在检查这些操作是否符合单子定律之后,我们可以说

一棵经过嫁接且没有简化的树是一个单子

展平

列表、袋子、集合、MaybeIdentity 将生成的树的所有级别扁平化为一个级别。任何嫁接在树上的东西都会出现在同一个列表、袋子或套装或Just 中。集合还会从生成的单层树中删除任何重复项。

          *
         / \
       [0,  1]

如果我们将它绑定到\x -> [x*2, x*2 + 1],我们会用新树替换每个叶子

        __*__
       /     \
      *       *
     / \     / \
   [0,  1] [2,  3]

然后将中间层展平

      ____*____
     /  |   |  \
   [0,  1,  2,  3]

我们可以这么说

扁平化的聚合是一棵经过嫁接和简化的树

而且,在检查了单子定律之后,我们可以说

扁平化的聚合是一个单子

修剪

Reader epairsdata Pair a = Pair a a 有点不同。他们无法将所有结果扁平化为单个图层,或者至少无法立即这样做。相反,他们会修剪与父级分支方向不同的分支。

如果我们从一对开始

          *
         / \
       <0,  1>

当我们将它绑定到\x -&gt; &lt;x*2, x*2 + 1&gt; 时,我们用新树替换每个叶子

        __*__
       /     \
      *       *
     / \     / \
   <0,  1> <2,  3>

我们修剪没有分支的分支

        __*__
       /     \
      *       *
     /         \
   <0,          3>

然后可以通过扁平化层来进一步简化这一点

          *
         / \
       <0,  3>

正如您所指出的,Reader e a 分支的方向数量等于e 的可能值数量。

我们可以这么说

修剪和扁平化的聚合是嫁接和简化的树

而且,在检查了单子定律之后,我们可以说

修剪和展平的聚合是单子

继续

continuation monad 是一棵树,每个可能的continuation 都有一个分支。我们将采用terminology Philip JF suggested for continuationscontinuation monad 是整个(a -&gt; r) -&gt; rcontinuation 是作为第一个参数传入的 a -&gt; r 函数

--                               continuation                                 
--                               |------|            
data Cont r a = Cont {runCont :: (a -> r) -> r}
--                               |-----------|
--                               continuation monad

延续单子的分支数量等于|r| ^ |a|,即延续a -&gt; r 的可能值的数量。每个分支都标有相应的功能。 每个叶子中的延续总是具有相同的值,我们稍后会证明这一点。我们还将为树的内部节点添加标签,这是一个函数r -&gt; r,我将在稍后讨论。

我们将使用以下数据类型来编写示例树。

data Tri = A | B | C

我们的示例树将用于return A :: Cont Bool Tri。树中保存的值的类型Tri 具有三个构造函数,而连续单子的结果Bool 具有两个构造函数。有2 ^ 3 = 8 可能的函数Tri -&gt; Bool,每一个都构成树的一个分支。

                               id *
      ____________________________|____________________________
false |     a |     b |     c |  aOrB |  aOrC |  bOrC |  true |
      A       A       A       A       A       A       A       A

"The way to a Monad's heart is through its Kleisli arrows"Kleisli arrows 是你可以传递给&gt;&gt;= 的第二个参数的东西;他们的类型为a -&gt; m b。我们将研究Cont 的Kleisli 箭头,其类型为a -&gt; Cont r b,或者,当我们查看Cont 构造函数时

a -> (b -> r) -> r

我们可以将连续单子a -&gt; (b -&gt; r) -&gt; r 的 Kleisli 箭头分成​​两部分。第一部分是决定将什么b 传递给延续b -&gt; r 的函数。它唯一需要使用的是a 参数,因此它必须是g :: a -&gt; b 函数之一。第二部分是合并结果的函数。它可以看到参数a 和将g a 传递给延续的结果。我们将调用第二个函数r :: a -&gt; r -&gt; ra -&gt; (b -&gt; r) -&gt; r类型的所有函数都可以写成形式

a  -> (b -> r) -> r
\x -> \f       -> r x (f (g x))

对于一些g :: a -&gt; br :: a -&gt; r -&gt; r

同样,每个延续单子(a -&gt; r) -&gt; r 都可以写成形式

(a -> r) -> r
\f       -> r (f a)

对于某些a :: ar :: r -&gt; r。结合这些构成了延续单子在每个叶子中始终保持相同值的理由。

当我们将函数 \x -&gt; \f -&gt; r x (f (g x)) 绑定到延续单子树上时,我们会将 g x 记录为新叶,并将 (r x, g x) 记录为新中间节点的标签。树真的很大,但我们将使用\x -&gt; \f -&gt; r x (f (g x)) :: Tri -&gt; (Bit -&gt; Bool) -&gt; Bool 绘制另一个完整示例的角,其中Bit 只有两个构造函数。生成的 continuation monad 应该只有 |Bool| ^ |Bit| = 4 分支,但我们还没有简化它。

                                            id *  
                _______________________________|_...
          false |                         a |
            r A *                       r A *
       _________|____________          _____|_...
bfalse |  b0 |  b1 |  btrue |   bfalse |  b0 |  
      g A   g A   g A      g A        g A   g A

由于每个叶子都具有相同的值,因此通过树的路径之间的唯一区别是标记每个分支的函数。我们将从分支中删除标签,只绘制一个分支。 return a 的第一个示例现在将绘制为

        id *
           |
           A

而将\x -&gt; \f -&gt; r x (f (g x))绑定到return A的例子会画成

        id *
           |
       r A *
           |
          g A

对于某些a :: ar :: r -&gt; r,任何以\f -&gt; r (f a) 形式编写的延续单子都将由树表示

         r *
           |
           a

绑定\x -&gt; \f' -&gt; r' x (f' (g' x))时,会用下面的树表示(这里只是嫁接)

           r *
             |
        r' a *
             |
           g' a

我们将从definition of &gt;&gt;= for Cont 中找出简化步骤。

m >>= k = Cont $ \ c -> runCont m (\x -> runCont (k x) c)

(\f -> r (f a)) >>= (\x' -> \f' -> r' x' (f' (g' x')))
= \c -> (\f -> r (f a)) (\x -> (\x' -> \f' -> r' x' (f' (g' x'))) x c) -- by definition
= \c -> (\f -> r (f a)) (\x -> (       \f' -> r' x  (f' (g' x )))   c) -- beta reduction
= \c -> (\f -> r (f a)) (\x ->                r' x  (c  (g' x ))     ) -- beta reduction
= \c ->        r ((\x -> r' x  (c  (g' x))) a) -- beta reduction
= \c ->        r (       r' a  (c  (g' a))   ) -- beta reduction
= \c ->       (r .       r' a) (c  (g' a))     -- f (g x) = (f . g) x
= \c -> (r . r' a) (c (g' a))                  -- whitespace

格式为\f -&gt; r (f a)。我们的树将被简化为

    r . r' a *
             |
           g' a

continuation monad 是一棵树,其内部节点用函数标记。它的绑定操作是树嫁接,然后是简化步骤。简化步骤将内部节点上的功能组合起来。可以这么说

延续单子是一棵具有嫁接和简化的树

而且,在检查了单子定律之后,我们可以说

延续单子是单子

【讨论】:

    【解决方案2】:

    我喜欢将延续视为其中有漏洞的程序。我想我最初是从 Tekmo's blog 那里收集到这个见解的。

    看看这个小延续:

    import Control.Monad.Trans.Cont
    
    program :: ContT () IO Char
    program = ContT $ \doThing -> do
      c <- getChar
      doThing c
    

    这是一个“缺少一块”的程序——即如何处理从getChar 检索到的Char。我们可以通过用putChar 之类的东西填充缺失的部分来运行它;通过runContT program putChar 评估延续将得到一个字符,然后将其打印到标准输出。

    如果您习惯于用抽象语法树来表示程序,那么容器类比可能会很直观。

    为了更清楚,您可以构建一个小 AST,其中包含一个 DoThing 术语,表示需要填充的孔:

    {-# LANGUAGE DeriveFunctor #-}
    
    import Control.Monad.Free
    
    data ExprF a =
        GetChar (Char -> a)
      | DoThing Char a
      deriving Functor
    
    type Expr = Free ExprF
    
    getChar' :: Expr Char
    getChar' = liftF (GetChar id)
    
    doThing' :: Char -> Expr ()
    doThing' c = liftF (DoThing c ())
    
    program' :: Expr ()
    program' = do
      c <- getChar'
      doThing' c
    

    program' 希望更清楚地是一个容器;要运行它,我们需要以与处理任何其他递归容器类似的方式处理 AST:

    eval :: Expr () -> (Char -> IO ()) -> IO ()
    eval prog f = iterM alg prog where
      alg (GetChar k)   = getChar >>= k
      alg (DoThing c k) = f c >> k
    

    通过eval program' putChar 评估program' 类似于通过runContT program putChar 运行program

    【讨论】:

    • 嗯,感谢您的洞察力和链接;但我仍然对如何将此 monad 视为聚合更感兴趣。也就是说,例如Reader e 不会立即被视为聚合,但是当您注意到函数空间e -&gt; a 只不过是幂空间a ^ e 时,您会注意到这只是@ 的序列“长度”|e| 的 987654338@。我的目标是像对延续单子的直觉一样无处不在的东西。感谢您的宝贵时间!
    猜你喜欢
    • 2011-10-22
    • 2012-02-14
    • 1970-01-01
    • 1970-01-01
    • 2012-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多