其实知道Continuation Monad还可以这么用还是因为CodeWars上的一道题。足足肝了三天还是没完全想明白orz。姑且先记录一下现在的想法吧。
先来回顾一下Continuation Monad的定义:
1 | newtype Cont r a = Cont { runCont :: (a -> r) -> r } |
从上一篇文章中我们知道f >>= g
不过就是f x
展开为一连串的apply形式:
1 | Cont r x == ($ x) |
这种形式到底有什么用呢?我们马上就能看到。
现在的问题是Coroutine该如何实现?我们可以把它抽象为一个指挥棒传递的过程。一个线程x
开心的运行着一些计算,直到它需要输入或输出一些数据。这个时候它把指挥棒交给另一个线程y
,直到y
产生一些输入输出,再交还指挥棒。等等,这个描述是不是和上面的展开式有点像?如果把每一个lambda表达式都看成一个计算的话,这正是我们需要的。于是根据语义我们可以写下
1 |
|
这里详细的解释一下。Coroutine
实际上和Cotinuation
是一个东西,只不过多了额外的两个类型参数。u
为输入参数的类型,d
为输出参数的类型。Command r u d a
里则定义了三种基本类型,分别为Done a
表示计算完成,没有输入输出;Out d (Coroutine r u d a)
表示输出类型为d
的数据,同时执行参数中的计算;In (\u -> Coroutine r u d a)
表示需要输入数据,并在等待输入时执行参数中的计算。很优雅的抽象,对吧?下一步就是将Coroutine
声明为Monad:
1 | -- useful alias |
这里在做的实际上和原来并没有太大区别,只是在每一个lambda表达式中多了一个判断。
- 如果计算完成了(
Done a
),那么直接进行下一个计算。 - 如果计算需要输出类型为
d
的数据(`Out d (Coroutine r u d a)),那么挂起这个计算,直到另一个计算返回结果,并将函数应用到这个结果上。 - 如果计算需要输入一个
u
类型,那么同样挂起计算,等到有输入的时候将输入传给另一个计算。
这样看起来可能很抽象,并且我们现在还没办法构建真正的”continuation” 。不妨定义一些helper function:
1 | output :: a -> Coroutine r u a () |
先来看output
,我们希望它立刻输出一些东西,于是可以写出:
1 | output :: a -> Coroutine r u a () |
然后是input
,它在获取一个输入后返回它:
1 | input :: Coroutine r v d v |
produce
连续的产生输出,而consume
将所有的Out
提取出来:
1 | produce :: [a] -> Coroutine r u a () |
filterC
挑选出每一个符合条件的输入并输出:
1 | filterC :: (v -> Bool) -> Coroutine r v v () |
最后,limit
类似于take
,suppress
类似于drop
:
1 | limit :: Int -> Coroutine r v v () |
现在,我们就可以用这些函数来构建一些有趣的东西了
1 | p1 = filterC even >>> limit 5 |
更多的玩法就留给读者去发掘吧w