延续作为有意义的理解

时间:2015-08-21 16:15:00

标签: haskell monads continuations

Monads可以被解释为容器的形式:

  • list:来自给定类型的项目的聚合
  • bag:无序聚合
  • set:忽略多重性的无序聚合
  • 也许:最多一个项目的聚合
  • 读者e:基数聚合| e |

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

2 个答案:

答案 0 :(得分:8)

我喜欢将延续视为带有漏洞的程序。我想我最初从Tekmo's blog收集了这个见解。

看看这个小延续:

import Control.Monad.Trans.Cont

program :: ContT () IO Char
program = ContT $ \doThing -> do
  c <- getChar
  doThing c

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

如果您对通过抽象语法树表示程序感到满意,那么容器类比可能是直观的。

为了更清楚,你可以建立一个包含DoThing术语的AST,代表一个需要填充的洞:

{-# 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

通过program'评估eval program' putChar类似于通过program投放runContT program putChar

答案 1 :(得分:6)

理解monad的真正关键是停止尝试说

  

monad X

而是开始说

  

X 是monad

如果是has a certain structure and obeys certain laws,那就是monad。出于在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的最大类事物。

嫁接树木

Monad的最大类是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

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

  

具有嫁接且没有简化的树是monad

平铺

列表,行李,集合,MaybeIdentity将生成的树的所有级别展平为一个级别。在树上任何地方嫁接的所有内容都会在同一个列表或包或集中或Just中结束。集合还会从生成的单层树中删除任何重复项。

          *
         / \
       [0,  1]

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

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

然后展平中间层

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

我们可以说

  

展平的聚合是一棵具有嫁接和简化的树

并且,在检查了monad法律后,我们可以说

  

扁平化的聚合是monad

剪枝

Reader e这样的{p> data Pair a = Pair a apairs有点不同。他们不能将所有结果压平成单层,或者至少不能立即这样做。相反,他们修剪那些与父母没有相同方向的分支。

如果我们从一对开始

          *
         / \
       <0,  1>

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

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

我们修剪那些没有分支同一方向的分支

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

然后可以通过展平图层来进一步简化这一过程

          *
         / \
       <0,  3>

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

我们可以说

  

修剪和展平的聚合是一棵具有嫁接和简化的树

并且,在检查了monad法律后,我们可以说

  

修剪和展平的聚合是monad

延续

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

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

continuation monad有许多分支等于|r| ^ |a|,即延续a -> r的可能值的数量。每个分支都标有相应的功能。 延续始终在每个叶子中保持相同的值,我们稍后会证明这一点。我们还将向树的内部节点添加标签,即函数r -> r,我将在稍后讨论。

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

data Tri = A | B | C

我们的示例树将用于return A :: Cont Bool Tri。树中保存的值的类型Tri具有三个构造函数,并且控制monad的结果Bool具有两个构造函数。有2 ^ 3 = 8个可能的函数Tri -> 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是你可以传递给>>=的第二个参数的东西;它们的类型为a -> m b。我们将研究具有Cont类型的a -> Cont r b的Kleisli箭头,或者当我们查看Cont构造函数时

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

我们可以将契约monad a -> (b -> r) -> r的Kleisli箭头分成两部分。第一部分是决定将b传递到延续b -> r的函数。它唯一需要处理的是a参数,因此它必须是函数g :: a -> b之一。第二部分是结合结果的功能。它可以看到参数a以及将g a传递给延续的结果。我们将调用第二个函数r :: a -> r -> r。类型a -> (b -> r) -> r的所有函数都可以以

的形式编写
a  -> (b -> r) -> r
\x -> \f       -> r x (f (g x))
某些g :: a -> br :: a -> r -> r

同样,每个延续monad (a -> r) -> r都可以用

形式编写
(a -> r) -> r
\f       -> r (f a)
某些a :: ar :: r -> r

。这些组合起来构成了一个继续monad总是在每一片叶子中保持相同价值的理由。

当我们将函数\x -> \f -> r x (f (g x))绑定到continuation monad树上时,我们将g x记录为新的叶子并记录(r x, g x)作为新中间节点的标签。树很大,但是我们将使用\x -> \f -> r x (f (g x)) :: Tri -> (Bit -> Bool) -> Bool绘制另一个完整示例的角落,其中Bit只有两个构造函数。由此产生的延续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 -> \f -> r x (f (g x))绑定到return A的示例将绘制为

        id *
           |
       r A *
           |
          g A

对于某些\f -> r (f a)a :: ar :: r -> r形式的任何续约monad将由树表示

         r *
           |
           a

当绑定到\x -> \f' -> r' x (f' (g' x))时,它将由以下树表示(这只是嫁接)

           r *
             |
        r' a *
             |
           g' a

我们将从definition of >>= 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 -> r (f a)。我们的树将简化为

    r . r' a *
             |
           g' a

continuation monad是一个树,其内部节点用函数标记。它的绑定操作是树移植,然后是简化步骤。简化步骤组成内部节点上的功能。我们可以说

  

continuation monad是一棵带有移植和简化的树

并且,在检查了monad法律后,我们可以说

  

延续monad是monad