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

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

作者: larluo_罗浩 | 来源:发表于2018-10-20 15:16 被阅读159次

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

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

一个函数里面进行各种操作的时候 ,怎样能够在特定情况下提前返回呢?
当然在面向过程语言里面,比较相似的就是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组合传递下去,特殊通道接受参数直接完成传递链

相关文章

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

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

  • 二、设计模式总览及工厂模式详解

    二、架构师内功心法之设计模式 2.架构师内功心法之设计模式 2.1.课程目标 1、通过对本章内容的学习,了解设计模...

  • 003-心法 2018-03-14

    总结:使用高级心法练就深厚内功 首先要区分心法与内功的区别 1、薄荷阅读 2、三十天练就。。 以上皆为心法 内功:...

  • 函数式内功心法-02: parser复合技术之parsec凌波微

    解析器复合技术简史 谈起haskell parsec,对于函数式编程的人来说,可谓无人不知,无人不晓。能轻轻松松2...

  • 敏捷与传统流程管理

    敏捷与传统流程管理理论的区别类似武功间的区别,传统流程管理类理论就是套路型拳法,而敏捷就是只包含几招配合的内功心法...

  • 终极主题营分享003

    学习之“易筋经” 《易筋经》乃少林派内功心法,习得此法,可改变筋骨,打通经络,提升武功修...

  • V2.1python 循环控制流

    程序编写有三种方式:流程式、函数式、对象式;今来说下流程式,其重点是其中的控制流。 现实中,做每件事都有个过程,有...

  • 古琴内功心法之古

    一曰古 理解:这里说的“古”,以个人的理解,是告诉我们,不要盲目的去追求。文章给出了多种对比,深入浅出。而《吕氏春...

  • 古琴内功心法之逸

    一曰逸 理解:逸乃安闲,安乐之意,现在多有超凡脱俗,卓尔不群之比喻。常形容一个人安逸,怡然自得的状态,是众人所向往...

  • 古琴内功心法之丽

    一曰丽 理解:很多人看到这个丽字可能都会认为是华丽,媚彩,我举个简单的例子,天下美人无数,你是喜欢浓妆艳抹,出入各...

网友评论

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

      本文链接:https://www.haomeiwen.com/subject/axlmzftx.html