函数式内功心法-01: 流程控制神器之callcc浪子回头

本人是一个函数式爱好者,苦于网上资料贫乏以及术语理论性太强久矣。
故本人决定用白话文讲述函数式技术,当然会不那么准确,但是便于理解其基本概念。

在函数式世界里,每个函数语句都要有返回值。

一个函数里面进行各种操作的时候 ,怎样能够在特定情况下提前返回呢?
当然在面向过程语言里面,比较相似的就是return语句了。
比如下面的例子:

whatsYourName :: String -> String
whatsYourName name =
  (`runCont` id) $ do                      -- 1
    response <- callCC $ \exit -> do       -- 2
      validateName name exit               -- 3
      return $ "Welcome, " ++ name ++ "!"  -- 4
    return response                        -- 5

validateName name exit = do
  when (null name) (exit "You forgot to tell me your name!")

对名称进行验证,验证失败后则直接返回。

这里在函数式是怎么做到的呢? 居然直接忽略后面的语句提前结束!

在函数式世界里面,正常情况下是不可能发生的。
除非我们有多通道,正常走正常的通道,特殊走VIP通道。
没说,我们就是可以这么干!

1. 首先我们引入CPS(continuation passing style)的概念。

什么是CPS?CPS就像一场接力比赛。
来看看简单的例子: 1 * 2 + 3 * 4
首先我们计算 1 * 2,然后计算3 * 4, 最后累加.
这里就涉及三次传递过程。
1 * 2 -> 3 * 4 -> a1 + a2 -> ?
每一次结果都往后传递处理,带上自己一直传递下去,这就是CPS风格。
谁来接最后一棒呢? 最后一棒有个特殊的名字,叫做终级continuation。

2. 我们再来看前面的问题

假如我们共有五次接力。假设第二次接力可能出现问题。
我们对第二次接力进行验证,如果没有问题,则继续往下接力。
如果有问题,直接找终级ccontinuation最后一棒!
所以问题似乎很简单了。。。

既然思想通了,那么就该开始练习内功心法了!

这里涉及haskell的两个库: transformer以及mtl。
mtl在transformer上对monad transformer做了增强。
mtl上面有个Control.Monad.Cont提供了callcc接口,用于实现VIP通道功能。
底层均由transformer的Control.Monad.Trans.Cont实现

相关源码如下:
mtl: https://github.com/haskell/mtl/blob/master/Control/Monad/Cont/Class.hs
transformer: https://hub.darcs.net/ross/transformers/browse/Control/Monad/Trans/Cont.hs

mtl本质上没干啥活,主义功能就是定义了一个MonadCont的接口(即typeclass)。其它的就是实现了底层Cont库的MonadReader跟MonadState接口。
所以,我们主要看transformer库。

1. 首先我们定义传递,这个传递主要由ContT定义

newtype ContT r m a = ContT { runContT :: (a -> m r) -> m r }

这里定义了ContT类型,主要有三个类型变量r, m, a
a是当前的传递值, m是对返回结果r进行隔离的另一层数据结构
整个过程就是对于已有的传递值a, 等待一个传递函数a->m r,最终生成结果m r。
对于函数类型来说,输入参数为等待值。这里等待a-> m r传递函数。
通过将自己的传递值a移交给传递函数,完成了传递功能 。
这里的ConT仅仅是一种函数类型封装,使用过程中则需要拆解以及再次封装过程。

2. 传递是如何组合的呢?

    m >>= k  = ContT $ \ c -> runContT m (\ x -> runContT (k x) c)
  1. 首先m传递\x值后,运行下一棒传递函数runContT (k x) c
  2. k绑定m的返回结果x后生成新的ContT,继续等待参数c传递函数
    因此,整 个过程比较简单,就是把当前结果\x传递给下一次调用(k x)后, 生成新的ConT继续等待终极传递函数。

3. 那么Cont又是如何构造的呢?

看一个简单的例子:
参见https://en.wikipedia.org/wiki/Continuation-passing_style

pow2_m :: Float -> Cont a Float
pow2_m a = return (a ** 2)

add' :: Float -> Float -> (Float -> a) -> a
add' a b cont = cont (a + b)

sqrt' :: Float -> ((Float -> a) -> a)
sqrt' a = \cont -> cont (sqrt a)

pyth_m :: Float -> Float -> Cont a Float
pyth_m a b = do
  a2 <- pow2_m a
  b2 <- pow2_m b
  anb <- cont (add' a2 b2)
  r <- cont (sqrt' anb)
  return r

两种Cont构造:

instance Monad (ContT r m) where
    return x = ContT ($ x)

cont :: ((a -> r) -> r) -> Cont r a
cont f = ContT (\ c -> Identity (f (runIdentity . c)))

pow2_m :: Float -> Cont a Float
pow2_m a = return (a ** 2)
pow2' :: Float -> (Float -> a) -> a
pow2' a cont = cont (a ** 2)
  1. 通过return构造, 调用$等待传递函数即得Cont Monad
  2. 通过cont函数调用,将a -> m r传递函数作为参数调用并进行等待

4. 传递函数如何传递呢?

newtype ContT r m a = ContT { runContT :: (a -> m r) -> m r }

runCont
    :: Cont r a         -- ^ continuation computation (@Cont@).
    -> (a -> r)         -- ^ the final continuation, which produces
                        -- the final result (often 'id').
    -> r
runCont m k = runIdentity (runContT m (Identity . k))

evalContT :: (Monad m) => ContT r m r -> m r
evalContT m = runContT m return

evalCont :: Cont r r -> r
evalCont m = runIdentity (evalContT m)

主要分为两种,
一种是runCount系列,就是接受一个终极传递函数即可
另一种是evalCont系列,即是将最后传递的值返回

callcc登场

前面的基础知识有了,让我们重新来回顾一下callcc的过程。

whatsYourName :: String -> String
whatsYourName name =
  (`runCont` id) $ do                      -- 1
    response <- callCC $ \exit -> do       -- 2
      validateName name exit               -- 3
      return $ "Welcome, " ++ name ++ "!"  -- 4
    return response                        -- 5

validateName name exit = do
  when (null name) (exit "You forgot to tell me your name!")

先看一下callcc是如何实现的

callCC :: ((a -> ContT r m b) -> ContT r m a) -> ContT r m a
callCC f = ContT $ \ c -> runContT (f (\ x -> ContT $ \ _ -> c x)) c
  1. 首先通过runCount运行一个callcc Cont, 并使用id作为终极传递函数返回Cont的传递值
  2. callcc调用一个函数,传递VIP通道(a -> ContT r m b)逃逸Continuation作为函数参数
  3. 逃逸Continuation接受一个传递值,直接调用终极传递函数后结束传递。
    (\ x -> ContT $ \ _ -> c x)
  4. callcc函数里面接受逃逸continuation之后进行CPS传递过程
  5. 正常逻辑一直通过monad组合传递下去,特殊通道接受参数直接完成传递链
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270