PL教程 第三章 (未完)
Table of Contents
(话说我打算逐步地讲如何编各种小游戏,然后知识点穿插在里面。这样应该会轻松一些吧。就这么决定了。)
手拉手
来接触一个新的定义,它表示平面里一个位置的坐标。(我是在为编游戏做铺垫了)
(struct posn ;; position (x y))
我们定义了一个 struct
,意为结构体,名叫 posn
,之后跟着一对括号,括号里为结构体存储的内容 x
和 y
。
实际上,我们写下这句话的时候,程序就为我们自动定义了一堆函数。首先,定义了一个同名的函数,就叫 posn
,它用来创建一个 posn
结构。(这个函数的名字和结构体的名字相同,有误导性,我觉得是个设计错误,请自己分清楚吧)
> (posn 1 2) ;; 一个位于(1,2)的坐标点 #<posn>
用 posn-x
和 posn-y
来拆开这个结构体,获得内部信息。用 posn?
来判断它是否为一个 posn
结构体。这些函数都是电脑自动帮我们定义的。
(define p (posn 1 2)) > (posn-x p) 1 > (posn-y p) 2 > (posn? p) #t
我再举几个简单的例子。
(define posn-abs ;; distance to (0,0) (λ (p) (sqrt (+ (sqr (posn-x p)) (sqr (posn-y p)))))) (define posn+ ;; add two 'posn's (λ (p1 p2) (posn (+ (posn-x p1) (posn-x p2)) (+ (posn-y p1) (posn-y p2))))) (define posn- ;; subtract two 'posn's ....) ;; <= as exercise (define distance (λ (p1 p2) (posn-abs (posn- p1 p2))))
你可以体会一下我是怎么用 posn-abs
和 posn-
组合成 distance
的。
先稍微总结一下,我们前两章学了通过函数组合出一个程序,我们说这叫“函数抽象”,现在我们又接触到了新的名词,叫“数据抽象(data abstraction)”。理解起来就是,一个 struct
是对数据的包装,就像函数是对程序逻辑的包装,我们要写上面那些跟 posn
有关的函数,当然可以不用定义 struct
,
传递参数的时候就分别传递横、纵坐标,比如
> (posn+ 1 2 100 200) 101 202
(先偷偷说一声,Racket 函数是可以返回多个值的)
但看起来实在是一团凌乱。四个参数加两个返回值,这是给人看的吗。
我高度近视,你知道的。
其实只要提供极少的数据结构,所有的程序就都能写了,只不过谁也不想看到本来一个简单的结构变成了一堆参数,还看不清楚它们都是干什么的。这是 struct
最基本的应用。但接下来就是一个颠覆一般人世界观的东西了(至少对我而言是这样,就是我前面说的第一次让我对编程感兴趣的东西,做好准备吧)
嗯,受到我看的编程书的影响,我继续拿吃的来举例吧。
假设我们为某个餐厅写一个点菜的程序,你当做我们是在编游戏也没问题。
;; a menu of dishes (define pizza "pizza") (define rice "rice") (define salad "vegetable salad") (define juice "orange juice") (struct order (table-ID dish)) ;; an example > (order 123 pizza) ;; guests at table 123 ordered a pizza #<order>
这应该是没问题的,接下来就有难题了。
一般来讲你去饭店是不会只点一道菜的,除非你说,“我的饭店的特色就是一桌只有一道菜,但一个 pizza 里包含了世界上所有的菜。” 以后要是你们中的谁开了一家这样的饭店,别忘了给我付版权费。
作为一家没什么创意的饭店,我们不得不满足各种顾客的需求
(struct order (table-ID dish1 dish2 dish3 dish4))
但理论上我们永远不能靠这样来解决问题。你可能觉得 4 个还不够,随着菜的数量增加,要再写 dish5
, dish6
……
甚至可能会一路写到 dish100
.
其实麻烦还不止这些,当你把这个订单发给厨师以后,厨师会发现他要反复调用
order-dish1
, order-dish2
, order-dish3
……
然后才能拿到所有点的菜。而且,从电脑的角度来说,大多数的订单并没有 100 道菜都点满,多数时候都有八九十道菜的内存是浪费的。
所以这条路应该是行不通的。我们的思路是要做到支持任意道菜。
请见下面这个结构
(struct dish-null ;; end of dish-list ()) (struct dish-list (dish rest)) ;; 'rest' is a dish-list or dish-null (struct order (table-ID dishes)) ;; 'dishes' is a dish-list
你可能比我聪明得多,所以不像我当时那么吃惊,或者你可能什么都没看出来,我们只是定义了 dish-list
和 dish-null
结构,dish-list
看起来再普通不过了,而 dish-null
有点奇怪,是个空的结构。神奇的就在于如何把它们组合起来。
先看一遍代码的注释。dish-list
就是我们想要的,可以存放任意个 dish
的结构。可是它只有两个参数?没关系。请回答,下面这段代码表示什么?
> (dish-null) #<dish-null>
答: 一个空的订单,什么都没有。
下面这个呢?
> (dish-list pizza (dish-null)) #<dish-list>
答: 一个只点了一个 pizza
的订单。
大胆猜想,
> (dish-list pizza (dish-list salad (dish-null)))
答: 一个点了 pizza
和 salad
的订单。
以此类推,
> (dish-list pizza (dish-list salad (dish-list juice (dish-list salad (dish-null)))))
答: 一个包含一份 pizza
, 两份 salad
和一份 juice
的订单。
我们来试着画出一个 struct
吧。
比方说,先试一下画出一个 dish-list
大致的结构
(因为它并不是一个函数(虽然有一个同名的函数叫 dish-list
),所以我暂且用圆角的长方形来表示它)
一个 dish-null
就很简单,什么都没有
那么 (dish-list pizza (dish-null))
就是
类似的,一个 (dish-list pizza (dish-list salad (dish-null)))
就是
以此类推,我懒得画了。
因为嵌套的结构太多,所以为了简单我们一般这样画
所以我们已经学会如何创造一份订单了!下面的问题是如何使用它
根据我们对 struct
的学习,我们定义一个结构体为 dish-list
时,我们就自动定义了一些函数来创建和拆开这个 struct
(struct dish-list (dish rest)) > (define x (dish-list 1 2)) > (dish-list-dish x) 1 > (dish-list-rest x) 2
(虽然不得不说,我觉得这个起名方式怪怪的,特别是那个 dish-list-dish
.
不过你喜欢的话,完全可以用 define
给它另起一个名字)
如何使用这些函数呢? 首先,我们可以用 dish-list-dish
函数来拿到第一道菜
(define d (dish-list pizza (dish-list salad (dish-null)))) > (dish-list-dish d) "pizza"
接下来,用 dish-list-rest
就能拿到剩下的订单,而结构是一模一样的。
> (define d-rest (dish-list-rest d)) ;; d-rest = (dish-list salad (dish-null))
所以,重复上述过程。再用 dish-list-dish
来得到 d-rest
中的第一道菜
> (dish-list-dish d-rest) "vegetable salad" > (dish-list-rest d-rest) #<dish-null>
如果菜单很长,就可以一直重复这个过程:
- 用
dish-list-dish
拿到当前的第一个菜 - 用
dish-list-rest
拿到剩余的菜单 - 重复这个过程
一直重复下去,总有一次会得到一个 dish-null
而不是 dish-list
于是这就表示已经没有更多的菜了,我们就停止循环。
这个过程让你想到了什么?
这又是上一章学过的递归。
我们对这个菜单能干些什么呢? 首先,除了把它的内容一个个拿出来以外,比方说,先数一数总共有几道菜。
写出函数 dish-list-length
, 输入一个 dish-list
, 输出它共有多少道菜。
下面是大致的框架
(define dish-list-length (λ (dishes) (if (dish-null? dishes) ___ (___ (dish-list-length (dish-list-rest dishes))))))
练习题: 请填空
首先,如果 dishes
是空的,那答案就是 0,很简单
(if (dish-null? dishes) 0 ....)
否则,我们就取出第一个 dish
,然后递归计算剩余的长度,再把它加 1
(+ 1 (dish-list-length (dish-list-rest dishes)))
就这么完成了。如果你上一章学得比较好,这里应该是没有任何问题的。
你是否想到了上一章中的 count-digits
?就是计算一个数有多少位数字的函数。
(define count-digits (λ (n) (if (single-digit? n) 1 (+ 1 (count-digits (delete-units-digit n))))))
它们是不是几乎一模一样?
我们把它改一改
(define zero? (λ (n) (= n 0))) (define count-digits (λ (n) (if (zero? n) ;; 个位数再删掉一位数字,就变成了 0 0 (+ 1 (count-digits (delete-units-digit n)))))) ;; compare with (define dish-list-length (λ (dishes) (if (null? dishes) 0 (+ 1 (dish-list-length (dish-list-rest dishes))))))
这下真的一模一样了,发现了没?他们都是计算一个东西的长度。
上一章,我们已经跟“数字列表”玩耍过,用 add-what
加上一个数字,用 get-units-digit
和 delete-units-digit
分别取个位数字,和更高位的剩余数字。这就是我们对“数字列表”所需要的全部了。
现在,我们又接触了“ dish-list
”,用 dish-list
这个函数,创建新的结构体,加上一个新 dish
,用 dish-list-dish
和 dish-list-rest
分别取第一道 dish
,和剩余的 dish
列表。这就是我们需要的全部了。
只要你理解了其中之一,你就都可以理解了。
但我很不爽,遇见重复的东西,我就想要把它提出来,单独变成一个函数。比如我要计算任意东西的长度
(define any-length (λ ....))
你可能不知道如何下手了。看清楚了
(define any-length (λ (x no-more? get-rest) (if (no-more? x) 0 (+ 1 (any-length (get-rest x) no-more? get-rest)))))
什么?后面两个 no-more?
和 get-rest
参数是什么玩意儿?
首先,我们用了 (no-more? x)
和 (get-rest x)
,也就是说,它们两个都是函数?
没错。看看怎么用
(define dish-list-length (λ (dishes) (any-length dishes dish-null? dish-list-rest))) (define count-digits (λ (n) (any-length n zero? delete-units-digit)))
够不够神奇?
你能明白这是怎么回事吗?函数就跟其它的东西一样,可以作为其它函数的参数,然后在其它函数中被调用。
上一节中,还有个 first-digit
函数,计算最高位的数字,我们试一下,能不能一模一样地,计算一个 dish-list
最后一道 dish
。
练习题: 写出 last-dish
函数。注意,这时候的判断条件就不是
dish-null?
了,因为要计算最后一道菜,那输入肯定是至少有一道菜的,不可能是个空的菜单,对吧。
(define first-digit (λ (n) (if (single-digit? n) n (first-digit (delete-units-digit n))))) (define single-dish? (λ (dishes) (dish-null? (dish-list-rest dishes)))) (define last-dish (λ (dishes) (if (single-dish? dishes) (dish-list-dish dishes) (last-dish (dish-list-rest dishes)))))
好了,来仔细地比较一下。
别的都没问题,唯一不同的是,当 if
条件成立的时候,前者是 n
,后者是 (dish-list-dish dishes)
,而不是简单的 dishes
。我估计你自己做完这道题,要是没测试过的话,这里很有可能是写错的。
其实也没错地很离谱,只不过我们再用 dish-list-dish
来获得一下那道
dish
就好了。所以,自己试试写个通用的函数。
(define last-element (λ (x stop? get-rest) (if (stop? x) x (last-element (get-rest x) stop? get-rest))))
(别忘了还要把 stop?
和 get-rest
原样递归进去,参数别漏了(这都是我自己犯过的错误啊))
练习题: 然后试试用它来定义 first-digit
和 last-dish
函数。
(define first-digit (λ (n) (last-element n single-digit? delete-units-digit))) (define last-dish (λ (dishes) (define one-dish-list (last-element dishes single-dish? dish-list-rest)) (dish-list-dish one-dish-list)))
我这里用了上一章最后讲的语法,就是函数中可以有内部的变量定义。我先计算了 one-dish-list
,然后输出了 (dish-list-dish one-dish-list)
(当然也可以不定义这个变量,直接把函数套在一块儿。但我个人喜欢多定义一些变量,这样思考起来更清楚。就像我第一章里说的,变量在一定程度上,能起到注释的作用)
怎么样,你没想到函数还能这么用吧。
还有很多很多的函数给你做练习题。你可以准备一下。这一节就再做最后一道吧:
写出函数 dish-list-member?
和 digit-member?
,分别计算第一个参数(一道菜或一个一位数字)是否在第二个参数(列表)中。写出一个通用的函数 any-member?
来定义它们。(你可以看到这些结尾是问号的函数,返回值都是 #t
或 #f
,也就是布尔类型的值)
上一节中讲过,判断是否相等的函数是 equal?
一个 if
判断显然是不够用的。你可能要多分一类讨论,在 if
中再嵌套一个 if
,请不要马上往下看,自己思考。
我就把 any-member?
给出来吧。
(define any-member? ;; search x in ls (λ (x ls no-more? get-elem get-rest) ;; elem: element (define mem? (λ (x ls) (if (no-more? ls) #f (if (equal? x (get-elem ls)) #t (mem? x (get-rest ls)))))) (mem? x ls)))
这里因为不停地传的参数太多了,我就在内部定义了一个函数,让代码看起来更简单一点。
当然这种函数已经是极限了(指它的参数个数),基本上我已经快眼花了,快要记不住哪个参数在第几个位置了。我一般是不会写出这样的函数的。如果参数再多两个,在实际中遇到的话,我宁愿复制粘贴代码。因为如果提出来一个函数,反倒更看不清了,折腾这个通用的函数变成负担了,那还不如傻一点,老老实实一个一个写。
现在你已经见识到函数绝大部分的威力了,只是你可能还不是很得心应手。目前的重点就是,学习怎样用好函数提供的这些能力。
函数永远是一个程序的中心。其它的东西都是死的,只有函数是活的。
我扯一些题外话吧。为什么说人类的语言比其它动物的语言高级?我认为,因为人类的语言有动词。动词跟程序中的函数是同一个概念。所谓的主语、宾语,不过都是动词的参数而已。
只有动词,能真正地造出任意复杂的句子,就像函数能任意地嵌套一样。当然,你也可以往名词上一个劲地叠形容词,但它造不出复杂的句子。每个人学英语的时候,都在语法上有一个难关,就是从句。从句能充当形容词,副词,名词,但充当不了动词,它们都是在为动词服务。
所以学外语的时候,我认为背单词,重点在于动词。把动词用漂亮了,能完胜那些只会堆形容词和副词的人。(说不定这句话能对你的英语作文有帮助)
而且学起语法来就没什么难度了。你心里很清楚它们是什么样的成分,只不过程序语言比较高级,可以像一个图形一样嵌套,自然语言只能把这些“结构上”的东西都抹平,然后用一些助词什么的来表示,其实它们只不过是在模仿程序语言里自然而然的结构。
我也不是说孰优孰劣,但我对语言的东西确实很感兴趣,我们是如何表达自己的意思的,我们是如何理解他人的信息的。
有时候我会梦想,希望人类能像三体人那样有透明的思维,这样世界上就不会有语言存在了,大家都能互相理解了,真是个美好的世界。(另: 作为学过 程序+语言 的人,我能看出《三体》的无数 bug…而且我一点也不喜欢这本书)
不过同时,你会发现个人不存在什么自由了。为什么呢,比如世界要毁灭了,需要牺牲你才可以拯救(这是什么中二情节)。你当然怕死,但是其它所有人的痛苦你都能感同身受,我认为,在这种情况下,你一定是很直接地选择自愿去死,而且并不会像地球的电影里拍的那样各种纠结、遗憾。
这其实等于你的思维、情感,已经是整个人类的一部分,你是没有什么自我意识的,整个人类已经组成了一个超级大脑,他们会诞生出一个真正的集体意识。你以为是你做出的决定,其实是这个集体意识做出的决定。为了集体可以牺牲部分。你具有的自我意识可能只是一种错觉。
(对于看过 Eva 的人,这就是那里面所说的人类补完计划。)
但人类有语言这个屏障,人是不可能理解他人的,人能不能理解自己都是个问题。这就是我这么希望研究语言的原因,自然语言,还有程序语言,音乐、美术的语言,甚至是电脑游戏的语言。我想找到那些能表达自己的东西,能在人之间建立起联系的东西。
所以在这一系列教程的前言里,我最开头就放上了巴别塔的故事。就这么一点传说,当时却给了我很大的触动。
但是问题也在于,这样世界真的会向美好的方向发展吗?现在信息技术发达多了,可以说,人类之间应该是联系更紧密了,所以人类应该更加互相理解了。
我认为是这样的,人们更能互相理解了,至少沟通的手段多了。但是这跟人的幸福似乎是相反的,似乎人之间的联系能够增加多少,人类这个集体的意识也会相应增加,人似乎就更听从于集体的决定,承担为了集体而给自己带来的痛苦。
你可以类比一下,你身上的每个细胞都像是一个独立的人,但是为了你的生存,它们不停的工作,甚至有的已经变异得认不出来了,就为了能让集体生存,要是这个集体死了,它们都会死。它们只能听从这个集体的意识,即使这会给它们带来痛苦。
所以我们是不是应该对组成自己身体的任何一部分,稍微多关爱尊重一些?
我不确定细胞也会有痛苦,至少它们的意识是跟我们完全不同的存在。但是可以猜想,如果人类这样发展下去,也许也会成为这样的一个整体,我们会诞生出整体的意识,人类在其中会失去自己,或者被免疫系统自动清理掉。
如果想保持个人的自由,我认为发展应该是有限度的,人类之间的联系增加,人类整体的发展,都是以牺牲个人为代价的。
但换个角度说,这也许又是人类进化的终极途径。试想一下,如果整个地球诞生出一个整体的意识,我们是它的细胞,这会是什么样子,我是无法想象的。
也许我臆想的成分实在太多,但是不管怎么说,我始终都在矛盾之中。我觉得世界上的痛苦都是人际关系导致的,我指的不是简单的与人交往,而是所有人之间,有意无意的互相影响。如果从古至今只有我一个人的存在,我就不需要面对所有的这些问题。
所以最一开始,单细胞生物诞生的时候,为什么它们要想到组合在一起,甘愿成为高级生命的一部分呢?为什么要牺牲自己来创造更高级的生命呢?世界上就自己一个细胞,快快乐乐地生存不好吗?
还是说,真的还是有什么自然规律在支配这些吗?
但是现在既然有这些问题了,交流似乎就是唯一解决问题的途径,交流都是通过各种形式的语言来完成。所以研究语言,似乎就可以解决我们所有的烦恼了。
问题就是,这个解决的结果,到底是人类快乐地共同生活,还是人类已经不作为一个个体存在?
也许两者都是,也无所谓快乐不快乐?
是的,我研究的是程序语言,而且研究地挺深了,我也喜欢接触其它各种语言,比如日语,见此链接。但在这期间,我也想了很多很多,我不知道这样的研究,和人类的发展,究竟会带来什么。我自己当然没有这么强的能力,让人类之间的联系获得巨大的进步,但是这样发展下去,总有一天会有这样的结果。人类以前认为发展是好的,现在认为在环保的前提下,发展是好的,有多少人明白发展到最后,到底会带来什么东西呢。在这之前,我们就一直这样研究下去吗。
集齐多米诺骨牌⏎
不行,我写教程还是要收一收,别的话还是放到博客的其它文章里再说吧。回归正题。
因为“列表(list)”在程序中实在太常见了,特别是在初学的时候,所以 Racket 其实已经提供了相关的一系列函数。
> '() '() > null '()
null
就是 Racket 提供的一个变量,它表示空列表。注意,它不是个函数,把它直接写出来就是空列表了。我也不清楚为什么不统一成函数,可能直接这么写比较方便吧。
至于 '()
是什么,你可以类比一下字符串,比如 ""
是空字符串,而 '(
一直到 )
就相当于那两个双引号,一个表示列表开始,一个表示列表结尾。只要在一个东西前面加个单引号 '
,里面的东西就不会被当成程序了,而会变成一个列表。
(我觉得这个语法有点难看,为什么不干脆用花括号 {}
呢?)
你可能已经被讲晕了,直接上例子吧
> (cons 1 '()) '(1) > '(1) '(1)
这是一个只含有 1
的列表。列表可以直接写出来 '(1)
,也可以用 cons
函数,它的意思是 construct
,就跟我们上一节中的 dish-list
用法一样,前一个参数是新列表的元素,后一个参数是旧列表。
接下来继续
> (cons 2 '(1)) '(2 1) > (cons 3 '(2 1)) '(3 2 1) ....
这样应该能看懂了。
需要特别提醒一下的是,列表跟字符串不同。字符串跟程序是一点关系也没有的,比如 "1"
跟程序中的数字 1
毫无关系,顶多只是看起来长得像罢了。但列表 '(1)
中的 1
就是程序中的数字 1
,我们把这个列表拆开之后,还能拿到这个数字 1
。
所以列表中也可以放任意的东西
> (cons "abc" null) '("abc") > (cons #t (cons 123 null)) '(#t 123) > (cons '(1) null) '((1)) ;; 列表中的列表 > (cons '(1) '((2 3))) '((1) (2 3))
你可以自己在电脑上多玩一玩。
当然还有把列表拆开的函数。
> (car '("what?")) "what?" > (cdr '("what?")) '() > (car '(1 2 3)) 1 > (cdr '(1 2 3)) '(2 3) > (car '()) ;; error > (cdr '()) ;; error
car
(就读作小汽车的那个 car),和 cdr
(读作 could-er),就是跟 cons
反着干的,它们分别取出 cons
的两个参数。
(至于为什么要叫这两个名字,据说是四五十(还是五六十)年前的的故事了,在古老的传说中,有一个叫 Lisp 的语言,它是 Scheme 语言的前身,然后 Scheme 又是我们现在用的 Racket 的前身。在那个 Lisp 语言里,好像有两个什么东西叫做 ‘a’ 和 ‘d’,然后不知怎么就有了这两个函数名,然后就流传下来了…)
(你要是不爽,可以用 first
和 rest
这两个函数,效果完全一样,是 Racket 帮你定义的别名,只是看上去没 car
和 cdr
高端罢了,每次写 car
和 cdr
,我就觉得自己在写上个世纪五六十年代的代码,特别带劲儿)
(好了又扯多了)
最后,用 null?
可以判断一个列表是否为 null
,与之相对应的,用 cons?
可以判断它是否是 不为 null
的列表,就是说,它是否是一个 cons
组成的。
估计你真的晕了,再总结一下,null
、cons
、car
、cdr
、null?
、cons?
,就这些。
下面你们就可以忘了 dish-list
和 digit-list
了,以后我们都只研究由 null
和 cons
组成的列表(list
)。但前面的东西能告诉你 list
是怎么来的。
练习题们: 试着用这些函数定义一下前面几节讲的函数。这几个函数就像加减乘除一样,会陪伴我们学编程的一生,你可以多写一些函数练练手。先不看那些通用的函数,自己重新写一遍下列函数,只需要处理由 cons
组成的列表就可以了。
length
,计算一个列表的长度。last
,返回列表的最后一个成员。member?
,判断第一个参数是否在列表中。
好了,继续,member?
里面用的是 equal?
来判断真假,还有比它更通用的,试着把参数 x
换成另一个参数 pred?
,然后把函数中的 equal?
换成 pred?
,(predicate
的缩写),新的函数叫 exist?
,说一说它有什么作用?
比如
(define >10? (λ (x) (> x 10))) > (exist? >10? '(1 2 3)) #f > (exist? >10? '(9 10 11)) #t
提醒一下第一章就学过的内容,一个函数只是定义给了变量而已,所以不必定义 >10?
这个变量,上面的代码可以写作
> (exist? (λ (x) (> x 10)) '(1 2 3)) #f > (exist? (λ (x) (> x 10)) '(9 10 11)) #t (define member? (λ (x lst) (exist? (λ (v) (equal? x v)) lst)))
你可以随心所欲地使用 λ
,把 λ
传来传去就是自然而然的事情一样。(这也是我选择 Racket 语言的原因,好多流行的语言都对函数有许多限制,让人觉得碍手碍脚的,只有 Racket(和少数语言,比如 Python)是真正的自由,这也是 Python 现在火起来的原因之一)
(而且 Python 在有些地方还比 Racket 好,但学到后面,写解释器的时候,你肯定就明白为什么我最后选择 Racket 了)
(所以说实话,现在推广学 Python 还是不错的,但问题是,根本就没那么多老师真正理解编程,我还没见过有课程是把最重要的知识: 函数调用,递归,列表等数据结构,之后就是程序的状态,然后是解释器,放在最前面先讲明白的。让学生花好多节课,背那些格式化字符串,还有各种循环语句,然后就是所谓的面向对象,几十节上百节课,背到最后,学生甚至连递归也不理解,更不要说闭包这种东西了)
(想知道“闭包(closure)”是什么?闭包就是函数代码+函数中引用的外部变量。所有的函数都是个闭包,这是第一章就应该能理解的内容了吧,函数不光要包含代码,还要保存它定义的时候,变量所在的位置。闭包就是这玩意。现在学了函数还能当参数传来传去,应该对它有更深的理解了。)
(又扯远了)
来吧,去 Racket 官方文档上瞅一眼,什么函数都有。比如,把上面的 exist?
的返回值从布尔类型,改成第一个满足函数的元素,就是官方的 findf
,比如 (findf >10? '(9 10 11 12)) => 11
再找一个做练习题: 写出 list-ref
函数(ref
为 reference
的缩写),参数为一个列表 lst
和一个自然数 pos
(position
),返回第 pos
个元素。
有一个有点摸不着头脑的地方,就是编程里的列表这类结构,都不是从 1 开始数的,而是从 0 开始数的。就是说,列表的第一个元素,在编程中叫做第 0 个元素。所以
> (list-ref '(1 2 3) 0) 1 > (list-ref '(1 2 3) 1) 2 > (list-ref '(1 2 3) 2) 3
别问我为什么,我也天天搞错这玩意。好像又是因为上古时代,大家还在写机器代码的时候,就是这么设计的。程序员数数也特别喜欢从 0 开始,就是数“自然数”,而不是“正整数”,应该也是受这个影响。比如阶乘函数,非要定义个 (factorial 0) => 1
,这样就可以从 0 开始递归了。谁知道为什么。
(不过说实话,在目前的电脑上,判断是否为 0,比判断是否为 1,好像效率高那么一点点…不过鬼才想管这种事情)
(不过似乎好多国家都习惯从 0 开始数?比如英式英语把 1 楼叫 the ground floor,2 楼才叫 the first floor)
(不过一切都从 0 开始数,很多东西真的会简单好多。比如说算公元前某年到现在的时间,或者数楼层,考试的时候少了好多坑诶)
所以就先记着吧,至于从 0 开始数是不是真的简单,我也不太肯定,但大家都是这么说的。养成从 0 开始数的习惯就好了。
我估计你已经忘记做上面那道题了,现在可以安心写 list-ref
函数了。我啰嗦的习惯要改一改。
然后是思考题: 如果输入的数字超过了列表的长度会怎么样?你可以自己试一试。是否能用 error
函数返回我们想要的错误呢?
你可以对比一下官方的 list-ref
函数。把你的函数整体注释掉,然后 Racket 本身已经提供了 list-ref
了(你能在文档上找到),你可以自己试一试。(再提示,直接在左括号前加 #;
可以整体注释掉整个括号的内容,这语法也挺好用的,我希望我不会讲得太快,让你语法记不过来了吧)
我保证,是最后一个练习题了。写出函数 all-satisfy?
,输入一个函数和一个列表,判断是否所有的元素都满足这个函数。这个函数跟前面的 exist?
是相对应的,exist?
也可以叫做
some-satisfy?
,就是“是否存在满足函数的元素”。
有没有感觉到逻辑学的臭味…
好了,如果说这一节是如何推倒多米诺骨牌,下一节就是,如何小心翼翼地把它们搭起来。