绪论
本文参考视频教程,教程给出了一系列编程题目。该教程作者裘香莲已经基于JavaScript,用lambda运算从头构建了一系列编程基础概念。本文则对其编程题以Common Lisp语言另外给出答案。
逻辑与判断的实现
代码展示
(defparameter *t* (lambda (opt1 opt2) `,opt2 (funcall opt1))) (defparameter *f* (lambda (opt1 opt2) `,opt1 (funcall opt2))) ;(defun *if* (cnd opt1 opt2) ;非惰性求值,使用时需要在选项处手动增加lambda封装 ; (funcall cnd opt1 opt2)) (defmacro lazy-if (cnd opt1 opt2) ;惰性求值 `(funcall ,cnd (lambda () ,opt1) (lambda () ,opt2))) (defun op-not(cnd) (lazy-if cnd *f* *t*)) (defun op-or (cnd1 cnd2) (lazy-if cnd1 *t* (lazy-if cnd2 *t* *f*))) (defun op-and (cnd1 cnd2) (lazy-if cnd1 (lazy-if cnd2 *t* *f*) *f*))
程序分析
其中,原*if*
(已被注释)以函数作为参数,这是《实用Common Lisp编程》中展示的一种编程特色,即某函数foo
本身只提供抽象框架,具体功能依靠以参数形式输入的函数method
决定。因此,可以通过控制函数method
,显著改变函数foo
的功能,从而实现高度可定制化。
考虑原*if*
、*t*
、*f*
的一种简单实现方式:
(defparameter *t* (lambda (opt1 opt2) `,opt1)) (defparameter *f* (lambda (opt1 opt2) `,opt2)) (defun *if* (cnd opt1 opt2) (funcall cnd opt1 opt2))
其中由于*if*
是一个函数,opt1、opt2的值在*if*
分支中,首先直接求值,然后将值传递进入函数*if*
。但这种实现方式有着重大问题:
- 在实际使用中,往往不希望if的某一分支实际执行
- 更多情形下,往往是为了避免某些操作的执行、减少意外,所以才使用if语句
为了解决这个问题,引入惰性求值,即传入*if*
函数的实际上是以lambda表达式包裹而形成的函数;而在设计*t*
、*f*
函数时,函数体中应当实际执行该lambda表达式,从而释放出被包裹的表达式。
(defparameter *t* (lambda (opt1 opt2) `,opt2 (funcall opt1))) (defparameter *f* (lambda (opt1 opt2) `,opt1 (funcall opt2))) (defun *if* (cnd opt1 opt2) (funcall cnd opt1 opt2))
但该实现仍然存在不便之处:引入惯性加载后,*if*
的分支需要手动用lambda表达式包裹,否则会导致执行错误。
为了解决这个问题,引入Common Lisp的概念:宏。与其它语言一样,Common Lisp的宏同样是一种文本替换。但由于Lisp语法本身的特点,Lisp语言的宏获得了极大的灵活性。甚至有人声称,Lisp宏具有无穷的可定制性。定义宏lazy-if
:
(defmacro lazy-if (cnd opt1 opt2) ;惰性求值 `(funcall ,cnd (lambda () ,opt1) (lambda () ,opt2)))
进而,可以借助lazy-if
来实现逻辑非、逻辑或、逻辑与。至此,逻辑与判断部分实现完毕。
自然数
代码展示
;;;; 自然数 (defparameter *0* (lambda (o) `(,o))) (defun *0*-p (num) (if (eql num *0*) *t* *f*)) (defmacro *1*+ (自然数) `(lambda () ,自然数)) (defun *1*- (自然数) (lazy-if (*0*-p 自然数) *0* (funcall 自然数))) (defparameter *1* (*1*+ *0*)) (defparameter *2* (*1*+ *1*)) (defparameter *3* (*1*+ *2*)) (defparameter *4* (*1*+ *3*)) (defparameter *5* (*1*+ *4*)) (defparameter *6* (*1*+ *5*)) (defparameter *7* (*1*+ *6*)) (defparameter *8* (*1*+ *7*)) (defparameter *9* (*1*+ *8*))
程序分析
对于自然数零的定义,是任意的。关键在于定义自然数后继的规则。
可以通过lambda表达式的一层包裹来代替后继。
(defmacro *1*+ (自然数) `(lambda () ,自然数))
类似地,前继则通过剥离lambda表达式来实现。其中,定义零的前继也是零。
(defun *1*- (自然数) (lazy-if (*0*-p 自然数) *0* (funcall 自然数)))
至此,自然数部分构建完毕。
算数
代码展示
(defun *1*-p (num) (lazy-if (*0*-p num) *f* (lazy-if (*0*-p (*1*- num)) *t* *f*))) (defun *+* (num1 num2) (lazy-if (*0*-p num2) num1 (*+* (*1*+ num1) (*1*- num2)))) (defun *-* (num1 num2) (lazy-if (*0*-p num2) num1 (*-* (*1*- num1) (*1*- num2)))) (defun *mul* (num1 num2) (labels ((multi-not-*0* (base times sum) (lazy-if (*1*-p times) sum (multi-not-*0* base (*1*- times) (*+* base sum))))) (lazy-if (*0*-p num2) *0* (multi-not-*0* num1 num2 num1)))) (defun *div* (num1 num2) (labels ((loop-er (num1 num2 商) (lazy-if (*<* num1 num2) 商 (let ((剩余 (*-* num1 num2))) (loop-er 剩余 num2 (*1*+ 商)))))) (lazy-if (*0*-p num2) *0* ; 任何数除以0均为0,可以接受 (loop-er num1 num2 *0*)))) (defun *eql* (num1 num2) (lazy-if (op-and (*0*-p (*-* num1 num2)) (*0*-p (*-* num2 num1))) *t* *f*)) (defun *<* (num1 num2) (lazy-if (op-not (*0*-p (*-* num2 num1))) *t* *f*)) (defun *>* (num1 num2) (lazy-if (op-not (*0*-p (*-* num1 num2))) *t* *f*))
程序分析
对于算数加,以以下步骤考虑:
- 考虑
num1+num2
,其中num2
=0,则结果为num1
- 考虑
num1+num2
,其中num2
≠0,则结果可以转换为(num1+1)+(num2-1)
- 递归进行第2步的转换,直到符合第1步的情形
类似地,实现算数减。
对于算数乘,以以下步骤考虑:
- 考虑
num1*num2
,其中num2
=0,则结果为0
- 考虑
num1*num2
,其中num2
=1,则结果为num1
- 考虑
num1*num2
,其中num2
≠0且num2
≠1,则问题可以转换为num1+num1*(num2-1)
- 递归进行第3步的转换,直到符合第2步的情形
对于算数除,以以下步骤考虑:
- 考虑
num1/num2
,其中num2
=0,则不可能存在结果,但为了程序的鲁棒性,规定结果为0
- 考虑
num1/num2
,其中num2
≠0,且num1
<num2
,则结果为0
(大小判断符号基于减号实现) - 考虑
num1/num2
,其中num2
≠0,且num1
≥num2
,则问题可以转换为1+(num1-num2)/num2
- 递归进行第4步的转换,直到符合第2步的情形
根据以上步骤,每种算术都能以简洁、美观的递归形式写出。但为了考虑到利用Common Lisp的尾递归优化,所以笔者在函数内通过labels宏定义了内部函数loop-er
。有些Common Lisp实现具有尾递归优化机制,例如笔者使用的SBCL。为了利用尾递归优化,所必须的是,函数体内在递归调用结束后不存在后续操作。
通过减法来实现大小判断符号,这里不再赘述。
斐波那契
(defun 展现-01 (自然数) (labels ((loop-er (自然数 disp-num) (lazy-if (*0*-p 自然数) disp-num (loop-er (*1*- 自然数) (1+ disp-num))))) (loop-er 自然数 0))) (defun 构造 (digit) (labels ((loop-er (自然数 digit) (if (> digit 0) (loop-er (*1*+ 自然数) (1- digit)) 自然数))) (loop-er *0* digit))) (defun 斐波那契 (第几个月) (lazy-if (op-or (*0*-p 第几个月) (*1*-p 第几个月)) *1* (*+* (斐波那契 (*-* 第几个月 *1*)) (斐波那契 (*-* 第几个月 *2*))))) ; (展现-01 (斐波那契 (构造 10))) ; => 89
链表 数组
代码展示
;; 双空间储物柜 (defmacro 柜 (物1 物2) `(lambda (勺) (funcall 勺 ,物1 ,物2))) (defparameter 勺1 (lambda (物1 物2) 物2 物1)) (defparameter 勺2 (lambda (物1 物2) 物1 物2)) (defun 取1 (柜) (funcall 柜 勺1)) ; 精简指令 (defun 取2 (柜) (funcall 柜 勺2)) ; (取1 (柜 'foo 'bar)) ; => foo ; (取1 (取2 (柜 'foo (柜 'bar 'baz)))) ; => bar ;; 链表 (defun 插 (链 elem) (柜 链 elem)) (defun 创-链 () (柜 *t* *t*)) (defun empty-p (链) (if (eql (取1 链) *t*) ; lazy-if 不能处理*t*, *f*以外的值 *t* *f*)) ; 展现-链 原实现方式: 已删除 (defun exist-链 (链 cond-p) (lazy-if (empty-p 链) *f* (let ((elem (取2 链)) (子 (取1 链))) (lazy-if (funcall cond-p elem) *t* (exist-链 子 cond-p))))) (defun find-链 (链 cond-p) (lazy-if (empty-p 链) *f* (let ((elem (取2 链)) (子 (取1 链))) (lazy-if (funcall cond-p elem) elem (find-链 子 cond-p))))) ; map-链, 逆-链 原实现方式: 已删除 ;; 优化 (defun reduce-链* (链 method init) "链表聚合;符合尾递归的写法" (labels ((loop-er (链 value) (lazy-if (empty-p 链) value ; 判断为空链 (let* ((子 (取1 链)) (elem (取2 链)) (new-value (funcall method elem value))) (loop-er 子 new-value))))) (loop-er 链 init))) ; (展现-01 (reduce-链* *12345* (lambda (pre next) (*+* pre next)) *0*)) ; => 15 (defun 展现-链* (链) "符合尾递归形式的展现链表 tail(原head) 被置于最后,这只是很小的一个代价" (reduce-链* 链 (lambda (elem value) (cons elem value)) '(tail))) ; (展现-链* (插(插(插(插(插(创-链) '上) '海) '自) '来) '水)) ; => (上 海 自 来 水 TAIL) (defun map-链* (链 cond-p) "符合尾递归形式的映射链表 逻辑的值并不直观(显示为函数的内存地址),所以改以符号的形式显示" (reduce-链* 链 (lambda (elem value) (let ((elem-boolean (if (eql *t* (funcall cond-p elem)) '*t* '*f*))) (cons elem-boolean value))) '(tail))) ; (defvar *12345* (插(插(插(插(插 (创-链) *1*) *2*) *3*) *4*) *5*)) ; (map-链* *12345* (lambda (elem) (*>* elem *2*))) ; => (*F* *F* *T* *T* *T* TAIL) (defun 逆-链* (链) "符合尾递归形式的逆序链表" (reduce-链* 链 (lambda (elem value) (插 value elem)) (创-链))) ; (展现-链* (逆-链* (插(插(插(插(插(创-链) '上) '海) '自) '来) '水))) ; => (水 来 自 海 上 TAIL)
说明
尽管上述代码能够构造一种类似于链表的数据结构,但实际上并不是链表,而可以看成一种数组。
这是因为,链表的实现通常需要依靠类似与指针或者引用的概念,但lambda演算中并没有这种概念。要想有类似的概念,需要首先构造出一个模拟内存,这会产生难以接受的复杂程度。
本文构造出的类链表数据结构,存在以下缺陷:
- 不能在指定位置删除结点
- 不能在指定位置插入结点
- 不能替换指定节点
- 不能将链表的尾部指向自己的头部
由于整个构造是以函数为基础的,而我们无法修改已经定义的匿名函数内已经存在的内容。因此,在进行删除、插入、替换操作时,需要将结点重新生成;另一方面,这种构造是一种嵌套的形式,表明需要将大片结点拆除、重建。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界