【Scheme】Scheme 编程学习 (二) ------ 基础
文接前一节内容 :
【Scheme】Scheme 编程学习(一) ------ 概述
本文章可以跟视频课程一起看,做了一些补充说明
原视频地址:
文章目录
- [【Scheme】Scheme 编程学习 (二) ------ 基础](#【Scheme】Scheme 编程学习 (二) —— 基础)
-
- [I - 概述](#I - 概述)
- [II - 数据结构](#II - 数据结构)
-
- [2.1 - 点对 Pairs](#2.1 - 点对 Pairs)
- [2.2 - 表 Lists](#2.2 - 表 Lists)
-
- [2.2.1 - 单元素表和空表](#2.2.1 - 单元素表和空表)
- [2.2.2 - 复合操作](#2.2.2 - 复合操作)
- [2.2.3 - list 过程](#2.2.3 - list 过程)
- [III - Loops](#III - Loops)
- [IV - 两种特殊的过程](#IV - 两种特殊的过程)
-
- [4.1 - Map](#4.1 - Map)
- [4.2 - Fold](#4.2 - Fold)
- [V - 注意事项](#V - 注意事项)
I - 概述
本节主要介绍 Scheme 的基础,包含以下四个部分:
- 两种数据结构点对 (Pairs) 和 表 (Lists),以及如何创建
- Loops
- 两种特殊的过程 (Procedures) Map 与 Fold
- 注意
Scheme 是一种函数式编程语言,什么意思呢?就是说它的参数可以为函数,返回值为函数,而且必须有返回值,若没有返回值,则接连的函数式无法继续执行下去。前一个函数的结果为后一个函数的参数。
而且 Scheme 是一种脚本语言,也就是类似 Python 需要解释器。不同于编译语言和伪编译语言。
II - 数据结构
2.1 - 点对 Pairs
lisp
(cons 1 2)
上述代码是调用过程 (Procedure) 的方式,( + 过程名称 + 参数 + ) , 过程与函数类似,调用过程可以理解为调用函数,这里过程名为 cons 用于创建点对,此调用结果为:
lisp
(1 . 2)
也可以使用嵌套的过程
lisp
(cons (cons 1 2) 3)
; ((1 . 2) . 3)
定义符号 (Symbol),使用 define 过程,定义符号 foo 为点对 (1 . 2)
lisp
(define foo (cons 1 2))
foo
; 检查 foo 输出 (1 . 2)
点对包含两个域 car 和 cdr
有两个同名的过程:
car
获取数据对象中的第一个元素
cdr
获取数据对象中的第二个元素
lisp
(car foo)
; 1
(cdr foo)
; 2
符号 foo 中第一个元素为 1 ,foo 中第二个元素为 2,car / cdr 过程不一定获取的是数据对象中的单个元素,也可以是复合的结构。
2.2 - 表 Lists
2.2.1 - 单元素表和空表
我们使用前一小节点对中的 cons 过程,构建一个特殊的点对,它的两个域,一个为 1 ,另一个为 null 也就是空,得到只有一个元素的点对,也是只包含一个元素的表。
lisp
(cons 1 null)
; (1)
定义 bar 符号为只有一个元素的点对,对其进行 car, cdr 操作
lisp
(define bar (cons 1 null))
bar
; 检测 bar 得到结果 (1)
(car bar)
; 1
(cdr bar)
; ()
空的括号 ()
是另一种表示 null 的形式,也是一个不包含任何元素的表。也就是说任何一个表最后一个元素都为 null 。
当我们对解释器输入这个特殊的符号 null 时,我们同样得到 空表
lisp
null
; ()
2.2.2 - 复合操作
lisp
(cons 1 (cons 2 null))
; (1 2)
创建一个包含 2 和 null 的点对,接着创建一个点对元素为 1 和刚刚创建的点对。获得一个点对 (1 2),但解释器表示为包含元素为 1 和 2 的表,也就是说表结构为很多点对的集合。
lisp
(cons 1 (cons 2 (cons 3 null)))
; (1 2 3)
使用 (cons 2 (cons 3 null))
可以获得一个包含 2 和 3 的表,使用结果作为右域与元素 1 创建点对,得到 一个包含元素为 1 2 3 的表。
定义一个符号 mylist 为包含元素 1 2 3 的表
lisp
(define mylist (cons 1 (cons 2 (cons 3 null))))
mylist
;(1 2 3)
(car mylist)
;1
(cdr mylist)
;(2 3)
对 mylist 应用 car 得到此表的第一个元素
对 mylist 应用 cdr 则得到所有剩下的元素
lisp
(cadr mylist)
; 2
cadr 表示首先进行 cdr ,然后进行 car
即
lisp
(car (cdr mylist))
cdr mylist 得到子表 (sublist) :(2 3)
接着 对此子表应用 car,也就是获取此子表的第一个元素,即 2
同样的
lisp
(caddr mylist)
; 结果为 3
cdr mylist 为 (2 3) ,对结果再次 cdr 为 3,car 此结果即为 3
速记:复合的左右域操作,顺序为从右向左。
2.2.3 - list 过程
我们也可以使用前一篇文章中见到的 list 过程来创建表。
lisp
(equal? (list 1 2 3) mylist)
过程 list 用于构建表,equal? 用于比较两个元素内容是否相等,比较使用 list 过程构建的表和使用 cons 构建的表是否相同,得到结果
lisp
#t
在 Scheme 中布尔值有两个 #t 和 #f , #t 表示 true, #f 表示 false
注意跟 C 语言或其他编程语言不一样的是
if (0)
与 if (#f)
不等价 if (0)
会进入 if 分支,if (#f)
则不会,
lisp
(list-ref lst n)
list-ref 是一个用于获取表中第 n 个元素的过程
lisp
(list-ref (list "a" "b" "c") 1)
; "b"
(list-ref (list "a" "b" "c") 2)
; "c"
list-ref 过程有两个参数,第一个参数为表,第二个参数为表中的第 n 个元素,与 C 语言相同,计数从 0 开始,当计数为 1 时,获取表中第二个元素,即 "b"
, 当计数为 2 时,则获取表中第三个元素,也就是 "c"
。
III - Loops
如何简略地实现 list-ref 过程?
lisp
(define (my-list-ref lst n)
(if (zero? n) ; 如果 n 为 0
(car lst) ; 返回 lst 的第一个元素
(my-list-ref (cdr lst) (- n 1))))
; 否则进行递归操作,获取 lst 除第一个意外子表的第 n-1 个元素
(my-list-ref (list "a" "b" "c") 1)
; "b"
(my-list-ref (list "a" "b" "c") 2)
; "c"
调用实现方式比较粗糙的 my-list-ref 过程,在参数相同时,得到相同的运行结果
此种实现方式被称为 "Loops" ,在其他的编程语言中称为 loop(循环)或 recursion(递归)
IV - 两种特殊的过程
4.1 - Map
(map procedure lst)
map 过程对参数表 lst 中的每一个元素应用过程 procedure 。
lisp
; 定义一个符号 baz 内容为包含 1 2 3 三个元素的表
(define baz (list 1 2 3))
; 定义一个过程 double ,结果为入参乘以 2
(define (double x) (* x 2))
(map double baz)
; (2 4 6)
对表 (1 2 3) 中的每一个元素应用 double 过程得到结果 (2 4 6)
lisp
; 定义一个过程 double-all
(define (double-all x) (map double x))
(double-all baz)
; (2 4 6)
这次只需要调用一个过程就可以得到相同的结果。
那么我们如何简略地实现 map 过程呢?
lisp
(define (my-map fn lst)
(if (null? lst)
null
(cons (fn (car lst)) (my-map fn (cdr lst)))))
(my-map double baz)
; (2 4 6)
my-map 的实现也是通过构建递归,首先如果为空则返回空,
lisp
if (null? lst)
null
判断是否为空 返回 null ,这里这里不仅是特殊情况处理,也是递归结束条件,如果处理到最后一个元素 (根据本文前面的内容,list 的最后一个元素为 null),则返回 null 结束
不为空 就会创建一个点对,此处应用到了 2.2 节中可以使用 cons 过程创建 list (表) 的内容
lisp
(cons (fn (car lst)) (my-map fn (cdr lst)))
生成表第一个元素为对表的第一个元素应用 fn ,(cdr lst)
获取除了第一个元素外,lst 中剩余的所有元素 因此,生成表的第二个元素也会获取原表第二个元素,并对此元素应用 fn ,依次类推。即对表中的所有元素都应用了 fn 。
4.2 - Fold
lisp
(define qux (list 1 2 3 4))
; 定义符号 qux 内容为 (1 2 3 4) 的表
(foldr + 0 qux)
; 结果为 10
调用 foldr 过程有三个入参, + 0 qux,0 为起始元素,然后将 0 与 qux 中的元素从后向前依次累加,即:
0 + 4
4 + 3
7 + 2
9 + 1
结果为 10
foldr 表示 fold up 堆叠,从最后一个元素开始一个一个叠起来。
有一些类似 C++ 标准库的 std::accumulate
函数,累加 需要指定初始值,因此
lisp
; 指定初始值为 2000
(foldr + 2000 qux)
; 结果为 2010
foldr 不仅可以指定累加 还可以指定其他过程,如
lisp
(foldr * 1 qux)
; 结果为 24
计算过程为:
1 * 4
4 * 3
12 * 2
24 * 1
结果为 24
同理
lisp
(foldr * 0 qux)
; 0
对于 map 同样的操作,我们如何实现 foldr
lisp
(define (my-foldr fn start lst)
(if (null? lst)
start
(fn (car lst) (my-foldr fn start (cdr lst)))))
(my-foldr + 0 qux)
; 10
(my-foldr * 1 qux)
; 24
首先判断是否为 null 与 my-map 一样,既是特殊情况处理,又是结束条件,返回 start
lisp
(fn (car lst) (my-foldr fn start (cdr lst)))
其他情况返回 计算 fn (第一个元素,剩余元素 与 start 元素fn 的结果) 。剩余元素仍然是 lst 则 返回 第二个元素与剩余元素 加 start 元素 fn 的结果...
以此类推,递归到最后一个元素时,则计算 最后一个元素 与 start 元素 fn 的结果。并返回上层调用,计算倒数第二个元素与这个结果的 fn 的值,然后返回更上一层调用...
举例 如果 lst 表中有两个元素 (1 2) , fn 为 +
则递归过程展开
lisp
(+ 1 (my-foldr + start (cdr lst)))
; 再展开
(+ 1 (+ 2 start))
; 即 start + 2 + 1
V - 注意事项
mzscheme 称空为 null ,其实是 nil
lisp
(define nil null)