UP | HOME

PL教程 第三章 (未完)

Table of Contents

(话说我打算逐步地讲如何编各种小游戏,然后知识点穿插在里面。这样应该会轻松一些吧。就这么决定了。)

手拉手

来接触一个新的定义,它表示平面里一个位置的坐标。(我是在为编游戏做铺垫了)

(struct posn ;; position
  (x y))

我们定义了一个 struct,意为结构体,名叫 posn,之后跟着一对括号,括号里为结构体存储的内容 xy

实际上,我们写下这句话的时候,程序就为我们自动定义了一堆函数。首先,定义了一个同名的函数,就叫 posn,它用来创建一个 posn 结构。(这个函数的名字和结构体的名字相同,有误导性,我觉得是个设计错误,请自己分清楚吧)

> (posn 1 2) ;; 一个位于(1,2)的坐标点
#<posn>

posn-xposn-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-absposn- 组合成 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-listdish-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)))

答: 一个点了 pizzasalad 的订单。

以此类推,

> (dish-list
   pizza
   (dish-list
    salad
    (dish-list
     juice
     (dish-list
      salad
      (dish-null)))))

答: 一个包含一份 pizza, 两份 salad 和一份 juice 的订单。


我们来试着画出一个 struct 吧。

比方说,先试一下画出一个 dish-list 大致的结构

dish-list-struct.png

(因为它并不是一个函数(虽然有一个同名的函数叫 dish-list ),所以我暂且用圆角的长方形来表示它)

一个 dish-null 就很简单,什么都没有

dish-null-struct.png

那么 (dish-list pizza (dish-null)) 就是

pizza-dish.png

类似的,一个 (dish-list pizza (dish-list salad (dish-null))) 就是

pizza-salad-dish.png

以此类推,我懒得画了。

因为嵌套的结构太多,所以为了简单我们一般这样画

linked-pizza-salad-dish.png

所以我们已经学会如何创造一份订单了!下面的问题是如何使用它

根据我们对 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>

如果菜单很长,就可以一直重复这个过程:

  1. dish-list-dish 拿到当前的第一个菜
  2. dish-list-rest 拿到剩余的菜单
  3. 重复这个过程

一直重复下去,总有一次会得到一个 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-digitdelete-units-digit 分别取个位数字,和更高位的剩余数字。这就是我们对“数字列表”所需要的全部了。

现在,我们又接触了“ dish-list ”,用 dish-list 这个函数,创建新的结构体,加上一个新 dish,用 dish-list-dishdish-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-digitlast-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’,然后不知怎么就有了这两个函数名,然后就流传下来了…)

(你要是不爽,可以用 firstrest 这两个函数,效果完全一样,是 Racket 帮你定义的别名,只是看上去没 carcdr 高端罢了,每次写 carcdr,我就觉得自己在写上个世纪五六十年代的代码,特别带劲儿)

(好了又扯多了)

最后,用 null? 可以判断一个列表是否为 null,与之相对应的,用 cons? 可以判断它是否是 不为 null 的列表,就是说,它是否是一个 cons 组成的。

估计你真的晕了,再总结一下,nullconscarcdrnull?cons?,就这些。

下面你们就可以忘了 dish-listdigit-list 了,以后我们都只研究由 nullcons 组成的列表(list)。但前面的东西能告诉你 list 是怎么来的。

练习题们: 试着用这些函数定义一下前面几节讲的函数。这几个函数就像加减乘除一样,会陪伴我们学编程的一生,你可以多写一些函数练练手。先不看那些通用的函数,自己重新写一遍下列函数,只需要处理由 cons 组成的列表就可以了。

  1. length,计算一个列表的长度。
  2. last,返回列表的最后一个成员。
  3. 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 函数(refreference 的缩写),参数为一个列表 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?,就是“是否存在满足函数的元素”。

有没有感觉到逻辑学的臭味…

好了,如果说这一节是如何推倒多米诺骨牌,下一节就是,如何小心翼翼地把它们搭起来。

┗>> 即可召唤魔法⏎