简单易懂的程序语言入门小册子(4):基于文本替换的解释器,递归,如何构造递归函数,Y组合子
递归。哦,递归。 递归在计算机科学中的重要性不言而喻。 递归就像女人,即令人烦恼,又无法抛弃。
先上个例子,这个例子里的函数double输入一个非负整数$n$,输出$2n$。 \[ {double} = \lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1)))) \]
现在的问题是,这个递归函数在我们的语言里没法直接定义。 我说的直接定义是指像这个用let表达式: \[ ({let} \; {double} \; \lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1)))) \; M) \] 把这个let表达式宏展开会看得更清楚些: \[ (\lambda {double}.M \; \lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1))))) \] $\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1))))$ 里的double是个自由变量。 解释器求值到这里的时候,根本不知道double指的是什么函数。
如何构造递归函数
获得递归的一个关键是如何在函数体中找到自己(结合一开始的比喻,这句话好像蕴含了其他意义深远的意思)。 一个简单的方法是在double上增加一个参数(一般就是第一个参数),把自己传入参数。 把这个修改后的函数叫做mkdouble1吧。 先不考虑mkdouble1的定义,先观察mkdouble1的行为。 因为调用mkdouble1要把自己作为第一个参数传入,所以调用递归函数应该这样写: \[ (({mkdouble1} \; {mkdouble1}) \; n) \] 也就是说,double就是$({mkdouble1} \; {mkdouble1})$。 \begin{eqnarray*} {double} &=& ({mkdouble1} \; {mkdouble1}) \\ &=& (\lambda v.(v \; v) \; {mkdouble1}) \end{eqnarray*} 最后一步变换是为了让mkdouble1只出现一次。
现在来考虑mkdouble1的定义。 在double上增加一个参数$f$: \[ {mkdouble1} = \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1)))) \] 函数调用的时候传入参数$f$的是mkdouble1。 也就是说$f$代表的是mkdouble1。 因此,函数体里递归调用的double用$(f \; f)$替换: \[ {mkdouble1} = \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ((f \; f) \; (- \; n \; 1)))) \] 所以double的定义是: \[ {double} = (\lambda v.(v \; v) \; \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ((f \; f) \; (- \; n \; 1))))) \] 这个定义可以用之前实现的解释器运行。 测试一下:
'(let double ((lambda v (v v))
(lambda f
(lambda n
(if (iszero n) 0 (+ 2 ((f f) (- n 1)))))))
(double 4)) >> 8
Y组合子
这一小节比较理论,知道个思路就行了。所以我就随便写写。 好学的人可以自己查资料(Programming Languages and Lambda Calculi, The Little Schemer)。
mkdouble1并不能让人很满意,因为它不优雅(都是时臣的错)。 mkdouble1递归调用的地方用的是$(f \; f)$,而比较好看比较符合直觉的应该只有一个$f$。 定义这个所谓的比较好看的函数mkdouble如下: \[ {mkdouble} = \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; (f \; (- \; n \; 1)))) \] 我们希望能从mkdouble得到递归函数double。 这是能做到的。只要在利用mkdouble1的double定义上做几个简单的推导就行了: \begin{eqnarray*} {double} &=& (\lambda v.(v \; v) \; \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ((f \; f) \; (- \; n \; 1))))) \\ &=& (\lambda v.(v \; v) \; \lambda f.(\lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; (f \; (- \; n \; 1)))) \; (f \; f))) \\ &=& (\lambda x.(\lambda v.(v \; v) \; \lambda f.(x \; (f \; f))) \; \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; (f \; (- \; n \; 1))))) \\ &=& (\lambda x.(\lambda v.(v \; v) \; \lambda f.(x \; (f \; f))) \; {mkdouble}) \end{eqnarray*}
$\lambda x.(\lambda v.(v \; v) \; \lambda f.(x \; (f \; f)))$被称作Y组合子,记为Y。 然后有: \[ {double} = ({Y} \; {mkdouble}) \]
Y组合子可以用来构造递归函数。 不过上面的定义在call-by-value的调用方式下会进入无限循环。 具体原因就不讲了,只讲结论:问题出在$(f \; f)$这里,对$(f \; f)$做一个$\eta$逆归约就行了。 修改后的Y组合子记为${Y}_{v}$: \[ {Y}_{v} = \lambda x.(\lambda v.(v \; v) \; \lambda f.(x \; (\lambda u.((f \; f) \; u)) \]
测试一下。 Call-by-value的测试:
'(let Y (lambda x
((lambda v (v v))
(lambda f (x (lambda u ((f f) u))))))
(let mkdouble (lambda f
(lambda n
(if (iszero n)
0
(+ 2 (f (- n 1))))))
((Y mkdouble) 4))) >> 8
Call-by-name的测试:
'(let Y (lambda x
((lambda v (v v))
(lambda f (x (f f)))))
(let mkdouble (lambda f
(lambda n
(if (iszero n)
0
(+ 2 (f (- n 1))))))
((Y mkdouble) 4))) >> 8
简单易懂的程序语言入门小册子(4):基于文本替换的解释器,递归,如何构造递归函数,Y组合子的更多相关文章
- 简单易懂的程序语言入门小册子(5):基于文本替换的解释器,递归,不动点,fix表达式,letrec表达式
这个系列有个显著的特点,那就是标题越来越长.忽然发现今天是读书节,读书节多读书. ==下面是没有意义的一段话============================================== ...
- 简单易懂的程序语言入门小册子(1):基于文本替换的解释器,lambda演算
最近比较闲,打算整理一下之前学习的关于程序语言的知识.主要的内容其实就是一边设计程序语言一边写解释器实现它.这些知识基本上来自Programming Languages and Lambda Calc ...
- 简单易懂的程序语言入门小册子(1.5):基于文本替换的解释器,递归定义与lambda演算的一些额外说明
这一篇接在第一篇lambda演算的后面.讲讲一些数学知识. 经常有些看似很容易理解的东西,一旦要描述得准确无误,就会变得极为麻烦. 软件工程里也有类似情况:20%的代码实现了核心功能,剩下80%的代码 ...
- 简单易懂的程序语言入门小册子(3):基于文本替换的解释器,let表达式,布尔类型,if表达式
let表达式 let表达式用来声明一个变量. 比如我们正在写一个模拟掷骰子游戏的程序. 一个骰子有6个面. 所以这个程序多次用到了6这个数字. 有一天,我们忽然改变主意,要玩12个面的骰子. 于是我们 ...
- 简单易懂的程序语言入门小册子(7):基于文本替换的解释器,加入continuation,重构解释器
或许在加入continuation之前要先讲讲费这么大劲做这个有什么意义. 毕竟用不用continuation的计算结果都是一样的. 不过,这是一个兴趣使然的系列,学习这些知识应该完全出于好奇与好玩的 ...
- 简单易懂的程序语言入门小册子(6):基于文本替换的解释器,引入continuation
当我写到这里的时候,我自己都吃了一惊. 环境.存储这些比较让人耳熟的还没讲到,continuation先出来了. 维基百科里对continuation的翻译是“延续性”. 这翻译看着总有些违和感而且那 ...
- Go语言入门篇-gRPC基于golang & java简单实现
一.什么是RPC 1.简介: RPC:Remote Procedure Call,远程过程调用.简单来说就是两个进程之间的数据交互. 正常服务端的接口服务是提供给用户端(在Web开发中就是浏览器)或者 ...
- C语言入门(2)——安装VS2013开发环境并编写第一个C语言程序
在C语言入门系列中,我们使用Visual studio 2013 Professional作为开发工具.本篇详细介绍如何安装Visualstudio 2013 Professional并写出我们第一个 ...
- 《Java从入门到失业》第一章:计算机基础知识(三):程序语言简介
1.3程序语言简介 我们经常会听到一些名词:低级语言.高级语言.编译型.解释型.面向过程.面向对象等.这些到底是啥意思呢?在正式进入Java世界前,笔者也尝试简单的聊一聊这块东西. 1.3.1低级语言 ...
随机推荐
- spring-boot(八) springboot整合shiro-登录认证和权限管理
学习文章:springboot(十四):springboot整合shiro-登录认证和权限管理 Apache Shiro What is Apache Shiro? Apache Shiro是一个功能 ...
- k8s网络之calico
一.概述 前面我们部署calico由于集群规模不是很大,使用的是calico的bgp模式的node-to-node-mesh全节点互联,这种模式在小规模集群里面还可以用,3.4.0版本的calico支 ...
- 完整例子-正则控制input的输入
转 : https://www.cnblogs.com/ckf1988/p/5619337.html
- 记一次SSM项目小结(一)
记一次SSM项目小结(一) ssm框架 环境配置 服务器配置 解决方法 拦截器重定向到localhost nginx和tomcat中session失效 mybatis的xml文件不生效 数据库用户创 ...
- 翻译:insert on duplicate key update(已提交到MariaDB官方手册)
本文为mariadb官方手册:INSERT ON DUPLICATE KEY UPDATE的译文. 原文:https://mariadb.com/kb/en/insert-on-duplicate-k ...
- JavaScript 二进制转文件
关于在javascript下,如何将二进制转换成相应的文件并下载. 首先, 我们需要得到二进制的数据以及相应的文件格式,没有相应的格式也可以,可以通过二进制来判断,但相对会麻烦很多,所以建议可以要求后 ...
- 数据可视化 seaborn绘图(1)
seaborn是基于matplotlib的数据可视化库.提供更高层的抽象接口.绘图效果也更好. 用seaborn探索数据分布 绘制单变量分布 绘制二变量分布 成对的数据关系可视化 绘制单变量分布 se ...
- Android Studio 活动启动模式
启动模式一共分4种 可以再配置文件中设置 <activity android:name=".MainActivity" android:launchMode="si ...
- JavaScript 获取按键,并屏蔽系统 Window 事件
// JavaScript 获取按键,并屏蔽系统 Window 事件 window.document.onkeydown = onkeydown; function onkeydown(event) ...
- node实现简单的群体聊天工具
一.使用的node模块 1.express当做服务器 2.socket.io 前后通信的桥梁 3.opn默认打开浏览器的模块(本质上用不到) 难点:前后通信 源码地址:https://github.c ...