• Scheme语言的“阴阳谜题”


    Scheme语言的“阴阳谜题”

    Scheme语言里有一个著名的“阴阳谜题(Yin-yang puzzle)”,大概是这么几行代码:

    (let* ((yin ((lambda (foo) (newline) foo)
                 (call/cc (lambda (bar) bar))))
           (yang ((lambda (foo) (write-char #/*) foo)
                  (call/cc (lambda (bar) bar)))))
      (yin yang))
    

    程序虽然短,但我第一次看见时,却说什么也猜不出它的运行结果。从表面上看,我大致知道call/cc会让程序陷入一个死循环,但实在不清楚循环内部到底是个什么逻辑。把程序拿到Scheme环境里一运行,吓了我一跳,结果居然是这么个样子:

    *
    **
    ***
    ****
    *****
    ******
    *******
    ...
    

    然后,我掰着手指头想了整整一天,才隐约明白了这个“阴阳谜题”的来龙去脉。看到网上谈这个谜题的人很多,详细拆解这个谜题的人却很少,我干脆把我对这个问题的理解写出来吧,不一定正确,仅供大家参考。

    首先要弄清楚的是,这个“阴阳谜题”程序到底是个什么结构。我们从里向外看:

    (call/cc (lambda (bar) bar))
    

    这一句把当前继续(Current continuation)传到匿名过程(lambda (bar) bar)中,而后者简单地返回传入的参数。也就是说,这一句的作用其实是获取当前继续。下面这样的组合

    ((lambda (foo) (newline) foo)
     (call/cc (lambda (bar) bar)))
    

    意味着把当前继续作为参数,传到匿名过程(lambda (foo) (newline) foo)中,而后者先输出换行符,再简单地返回传入的参数。再进一步:

    (yin ((lambda (foo) (newline) foo)
          (call/cc (lambda (bar) bar))))
    

    它的作用是在输出换行符后,令局部变量yin绑定到call/cc得到的继续。对yang的绑定也类似。而下面这句

    (yin yang)
    

    就是在当前的yin和yang的绑定环境中,以yang为参数调用yin。因为在Scheme中,所谓的继续(Continuation)就是一个过程,既可以被调用,也可以扮演参数的角色。所以,(yin yang)这样的语法是没有任何问题的,关键是(yin yang)这样的调用会产生什么结果,这就不是一眼可以看出来的了(也许是我自己太愚钝,聪明人多半可以很快找到答案的)。

    算了,既然一眼看不出来,就先把(yin yang)这样的怪代码放到一边。整理一下思路,整个“阴阳谜题”实际上做了这么几件事:

    ① 开始执行(let*)结构
    ② 获得当前继续
    ③ 输出换行符
    ④ 把②获取的继续赋予yin
    ⑤ 获得当前继续
    ⑥ 输出星号
    ⑦ 把⑤获取的继续赋予yang
    ⑧ 以yang为参数调用yin
    

    看上去就是这样8个步骤,但为了参透其运行逻辑,必须确定两件事:

    1、(let*)到底是个什么结构?

    这个问题用不着我来回答,Scheme书籍和资料里讲得很多了。只讲一点最重要的:(let*)中有一个变量列表,如这里的yin和yang,(let*)会先把第一个变量的绑定创建好,然后在第一个变量的绑定已知的环境内,把第二个变量的绑定创建好,依此类推,直到所有绑定创建好以后,就在包含所有这些变量绑定的环境内求得主体表达式的值。在本例中,主体表达式就是简单的一句话:(yin yang)

    2、步骤②和⑤中获取的到底是什么继续?

    如果读者搞不清什么是继续,或不清楚如何使用继续的话,最好先去查书查资料。我只强调一下,call/cc得到的继续其实就是call/cc所在的当前表达式的“全部未来”。“全部未来”这个字眼儿是从Scheme的标准文档R5RS里抄来的,它的意思是说,平常没有call/cc时,我们在求得表达式的值后要做什么事情,现在直接调用call/cc的结果过程时就会做什么事情。

    也就是说,如果②处不是一个call/cc而是一个普通表达式,那么我们在求得表达式的值后,会做这些事情:输出换行符,把该表达式的值赋予yin,创建一个包含yin的新环境,然后在新环境中完成后续的步骤——我把这件事记作(A)。

    现在,代码在②处获得了当前继续。这意味着,一旦我们在今后调用这个继续,调用时就会重复同样的步骤,而这时使用的“表达式的值”,就是我们在调用继续时传入的那个参数。

    类似的,如果调用⑤处得到的继续,就会做这些事情:输出星号,把该表达式的值赋予yang,创建一个包含yang的新环境,然后在新环境中完成后续的步骤——我把这件事记作(B)。

    好了,弄清楚这两个问题后,我们可以试着手工运行一遍“阴阳谜题”了。在下面的解析中,我们把②处获得的继续称为Ca,把⑤处获得的继续称为Cb:

    1、获得继续Ca,输出换行符,把Ca赋予yin,我把这些步骤简记作:

    	/	yin = Ca(_)
    

    其中,/ 表示输出换行符,Ca(_)表示②处获得的继续,而在将该继续赋予yin之前,yin的值未定义,所以括号内简记为 _

    2、在包含yin的环境中,获得继续Cb,输出星号,把Cb赋予yang,记作:

    	*	yang = Cb( Ca(_) )
    

    其中,Cb( Ca(_) )表示⑤处获得的继续,在将该继续赋予yang之前,yin的值是Ca(_)

    3、调用(yin yang)。从刚才的分析,我们可以知道,这个调用要做的事情是,把yang绑定的继续Cb( Ca(_) )作为参数,调用yin绑定的继续Ca(_)。套用刚才分析出来的那句话(A),就是:输出换行符,把Cb( Ca(_) )的值赋予yin,创建一个包含yin的新环境,然后在新环境中完成后续的步骤。记作

    	/	yin = Cb( Ca(_) )
    

    4、现在要“完成后续的步骤”了。后续的步骤是⑤,所以接下来应该是:

    	*	yang = Cb( Cb( Ca(_) ) )
    

    5、又到了(yin yang)这一句了。现在yin和yang中绑定的过程和上一次不太一样了。最重要的是,yin中绑定的是Cb( Ca(_) )而不是Ca(...),这表示对yin的调用将要做(B)描述的事情,而不是(A)描述的事情。我们如法炮制,现在这个(yin yang)的意思是:输出星号,把Cb( Cb( Ca(_) ) )赋予yang,创建一个包含yang的新环境,然后在新环境中完成后续的步骤。记作

    	*	yang = Cb( Cb( Ca(_) ) )
    

    有一个问题是,现在的yin该是一个什么东西呢?注意,调用前,yin的值是Cb( Ca(_) ),我发明的这种记法表明了创建这个继续时的环境,即,获得这个继续时,yin的值是Ca(_)。而我们现在调用yin,从某种意义上说,这相当于回到了创建这个继续时的环境里,我们可以简单地认为,调用后,yin的值又变回了Ca(_)。记作(这里给出的只是“近似”的说法,后面我们会讨论yin的值为什么会变回去):

    		yin = Ca(_)
    

    6、这回马上就又碰到了下一轮的(yin yang),因为yin的值是Ca(_),所以我们会回到(A)描述的事情中,记作:

    	/	yin = Cb( Cb( Ca(_) ) )
    

    7、就像这样“运行”下去,再多写几步:

    	*	yang = Cb( Cb( Cb( Ca(_) ) ) )
    	*	yang = Cb( Cb( Cb( Ca(_) ) ) )
    		yin = Cb( Ca(_) )
    	*	yang = Cb( Cb( Cb( Ca(_) ) ) )
    		yin = Ca(_)
    	/	yin = Cb( Cb( Cb( Ca(_) ) ) )
    	... ...
    

    把我们上面7步得到的输出连起来:

    /
    */
    **/
    ***/
    ...
    

    这不就是“阴阳谜题”的运行结果吗?好像事情还比较顺利,但我们还有一个问题没解决:当yin的值为Cb(...)时,在(yin yang)调用中,yin的值为什么会变回去?这个问题和Scheme使用的环境模型有关。建议大家回想一下《计算机程序的构造和解释(SICP)》一书第3章的内容。我仿照SICP的样子把“阴阳谜题”里的环境变化情况解释一下,在下面这两幅图中:

    1、进入(let*)前,只有一个初始环境。把Ca绑定到yin,并创建包含yin的环境,这实际上相当于创建了一个包含yin的子环境,子环境中有个指针指向初始环境。就是左图中绿色的yin。其中,实线箭头表示引用父环境,虚线箭头表示变量绑定。

    2、在刚创建的子环境中,把Cb绑定到yang,并创建包含yang的新的子环境,新子环境中有个指针指向包含yin的子环境。这就是左图中绿色的yang。

    3、(yin yang)调用时,因为yin绑定的Ca过程的含义是重新创建包含yin的环境,所以,左图又多出了蓝色的yin,但这时yin的绑定指向绿色的Cb。接着创建蓝色的yang,它指向一个新的Cb。现在我们使用的是蓝色的yin和yang。

    4、下一次(yin yang)调用,因为yin的绑定指向绿色的Cb,其含义是在绿色yin的环境下,重新创建包含yang的环境。于是,绿色yang被新创建的红色yang取代,而红色yang的绑定指向蓝色的Cb,红色yang的父环境还和绿色yang一致,是绿色的yin。这就是右图中的样子。现在,我们使用的是绿色的yin和红色的yang。也就是说,刚才还在使用指向Cb的yin,现在又恢复成使用绿色的yin了。这就是yin的值为什么会变回去的原因所在了。同时,因为红色yang这时指向了蓝色的Cb,下一次yin会变回到蓝色的yin,再下一次才会变回绿色的yin,因此,“阴阳谜题”每一行都会比上一行多输出一个星号。

    这就是我推理出来的“阴阳谜题”的答案了(上面的讲解只是一种概念模型,与Scheme的具体实现并不完全等同)。不过,这个答案是手工推理出来的,能够被程序自动证明吗?应该是可以的,我把“阴阳谜题”扩展了一下,让程序可以自动跟踪每个继续的语义,并自动打印输出。修改后的代码如下:

    (define cc-dict '())
    (define (insert-cc! cc flag)
      (if (assq cc cc-dict)
          #f
          (set! cc-dict
                (cons
                 (cons cc (cons flag (length cc-dict)))
                 cc-dict))))
    (define (display-cc cc prefix)
      (display prefix)
      (display #/()
      ((lambda (cc-pair)
         (cond (cc-pair
                (display (cadr cc-pair))
                (display #/,)
                (display (cddr cc-pair)))))
       (assq cc cc-dict))
      (display #/)))
    
    (let ((count 5) (yang #f))
      (call/cc
       (lambda (exit)
         (let* ((yin ((lambda (foo)                    
                        (write-char #//)
                        (newline)
                        (insert-cc! foo #/a)
                        (display-cc foo "yin")
                        (display-cc yang "yang")
                        (set! count (- count 1))
                        (if (= 0 count) (exit 'end) foo))
                      (call/cc (lambda (bar) bar))))
                (yang ((lambda (foo)                     
                         (write-char #/*)
                         (insert-cc! foo #/b)
                         (display-cc yin "yin")
                         (display-cc foo "yang")
                         foo)
                       (call/cc (lambda (bar) bar)))))
           (yin yang)))))
    

    上述代码显示了“阴阳谜题”前5行的运行过程,每次为yin、yang赋值前,代码都把yin、yang的内容打印出来,打印格式为 yin(a,0)yang(b,1) 或类似的格式,其中,yin(a,0) 表示 yin 的值为继续Ca,该继续是程序生成的第1个继续(基于0的索引)。上述代码的运行结果为:

    /
    yin(a,0)yang()*yin(a,0)yang(b,1)/
    yin(b,1)yang()*yin(b,1)yang(b,2)*yin(a,0)yang(b,2)/
    yin(b,2)yang()*yin(b,2)yang(b,3)*yin(b,1)yang(b,3)*yin(a,0)yang(b,3)/
    yin(b,3)yang()*yin(b,3)yang(b,4)*yin(b,2)yang(b,4)*yin(b,1)yang(b,4)*yin(a,0)yang(b,4)/
    yin(b,4)yang()
    

    从这个运行结果里,我们可以清楚地看到yin、yang的变化情况,也可以看到在每一行中,yin是如何一步步地变回原来的值,并最终变回Ca以输出换行符的。

  • 相关阅读:
    阿里云如何跨账号打通,多账号同时享受优惠
    阿里云超长6个月免费套餐汇总
    零门槛申请阿里云1万元代金券
    阿里云包年包月服务器如何退款
    用户名密码注册与登录的应用
    Android 沉浸式状态栏完美解决方案
    Android开发支付集成——微信集成
    Android开发支付集成——支付宝集成
    ExpandableListView的完美实现,JSON数据源,右边自定义图片
    android扫描网页二维码进行网页登录
  • 原文地址:https://www.cnblogs.com/xiaomaohai/p/6157257.html
Copyright © 2020-2023  润新知