UP | HOME

PL教程 第二章 停下来!

Table of Contents

首先声明一下,虽然对数字加加减减没什么意思,但要是一上来学别的东西 会更吃不消,把这些基本概念学好了以后,大约下一章就可以摆脱数字了。

不同的包装

猜一猜这是什么

> (= 123 123)
#t ;; t 代表 true,中文一般叫做“真”
> (= 123 100)
#f ;; f 代表 false,“假”
> (> 123 100)
#t
> (< 123 100)
#f
;; 还有 >= 和 <=,它们都是函数

很明显,它们都是比较数字大小的,分别是等于,大于,小于,大于等于,小于等于。(不得不说,看起来实在是怪怪的,我直到现在也觉得很难看)

它们的返回值都是 #t#f,这两者又是跟数字、函数一样的东西,被称为布尔值(boolean)。(比如说 123 是数字类型,#t#f 就是布尔类型)。我们把数字类型、布尔类型、函数类型,和之后会接触到的其它的类型,统称为“数据类型(data type)”

Racket 居然没有不等于函数,但有 not 函数,作为练习,请自己定义一下不等于函数。

> (not #t)
#f
> (not #f)
#t

(define =/= ;; 如果有谁想出了更好看的符号,告诉我一声
  (λ (x y)
    (not (= x y))))
> (=/= 1 2)
#t

然后观察一下这些函数,它们跟我们之前讲的函数有些不一样。

num-and-bool.png

第二行的那个函数不同,不是因为返回值从数字变成了布尔。
而是因为它们把数字和布尔,这两种不同的类型混合起来了。

从上一章的内容中可以体会到,函数都好像工厂里,流水线上的机器一样。产品从一边流入,经过加工(有些是对一个产品加工(一个参数),有些是把一些产品揉在一起(多个参数),有些是多个参数,但有主次之分),经过加工,把加工后的产品送出来。把函数组合起来就能完成程序。

但这些函数与之前所说的概念不太一样,它们只消耗了输入,但没有对它们加工,而是输出了一个判断条件,这是为了控制程序之后的走向。所谓的布尔值 #t#f 就是因此诞生的。

来看一下如何使用它们

> (if #t 1 2) ;; 若 #t 则 1 否则 2
1
> (if #f 1 2)
2
> (if (= 100 100)
      (* 1 2 3) ;; 只要是值的地方都可以换成函数运算
      456)
6
> (if (>= 123 456)
      (not (= 1 2))
      (+ 1 2)) ;; if 的两个分支的类型不同也可以,
               ;; 虽然一般来讲没什么意义
3

if 类似于有3个参数的函数,第一个参数是判断条件,后两个参数是两个分支。第一个参数必须为 #t#f,如果为 #t 则值为第二个参数,如果为 #f 则值为第3个参数,受到其它语言的影响,习惯上记忆为:“如果…那么…否则…” (if...then...else...)

不过要注意的是,if 不是一个函数。还记得吗,函数在调用之前,要先把参数都算出来。但很显然,if 不需要。如果第一个参数为 #t,那么只需要计算第二个参数就可以,第三个参数不管是多复杂的式子,都可以直接扔掉。如果第一个参数为 #f 那就只要计算第三个参数。所以 if 被设计成了一个特殊的语法,而不是一个函数。

练习题: 写出绝对值函数 abs,如果是正数或零则不变,负数则变正数。补充: - (减法函数)可以只有一个参数,(- 123)-123

答案有很多种,这是其中一种答案:

(define abs
  (λ (x)
    (if (>= x 0)
        x
        (- x))))

继续练习题: 写出 maxmin 函数,都接受两个参数,分别返回参数中较大的一个和较小的一个。

如果熟悉语法了就可以了。

我们还可以把 if 用于检查错误。比如说,如果给上面的 abs 函数输入一个布尔类型的值

> (abs #t)
; >=: contract violation
;   expected: real?
;   given: #t
;   argument position: 1st
;   other arguments...:
;    0

这个错误信息表示我们调用了 >= 函数,然而它出错了(contract violation),下面几行就是,它要一个实数(real),然而我们给了它一个 #t,是它的第一个参数,它还有一个参数是 0。现在看不懂这些没关系。反正这会让使用者很奇怪,为什么我调用了 abs 但给了我一个跟 abs 毫不相关的错误?

所以在实际中,一般都会自己检查这类错误,防止让使用者知道,你这个函数原来偷偷调用了 >= 函数。

(define abs
  (λ (x)
    (if (number? x)
        (if (>= x 0)
            x
            (- x))
        (error "abs: number expected, given:" x))))

(注: 严格来说应该用 real? 而不是 number? 是因为 Racket 里的数字 可以是虚数,但反正你又用不到虚数,不用管它)

这里有好多新东西,一点一点来看。

首先,number? 是一个函数,判断参数是否为数字,跟它相似的还有 boolean? (是否为布尔类型),procedure? (是否为函数), string? (是否为字符串),字符串下面就讲。

如果 x 为数字,那么就照常计算 x 的绝对值,如果不是就报错。

error 怎么说呢,它也是个函数,但跟一般的函数意义上又不一样了,一旦调用这个函数,不管你在程序的什么位置调用的它,程序都会直接出错。它的参数就是出错信息了,第一个参数一定要是个字符串,剩下可以有任意个参数,随便什么都可以,统统是出错信息。

至于字符串。我们把一个字(母)叫做一个“字符”,而一串字符就叫“字符串”。下面这些都是字符串

> "abc"
"abc"
> "this is a string" ;; 可以有空格
"this is a string"
> "" ;; 空的字符串
""
> "!@#$%^ &*()~ `[]-=';" ;; 基本上什么符号都行
"!@#$%^ &*()~ `[]-=';"
> "(+ 1 2)" ;; 这跟程序的 (+ 1 2) 一点关系都没有
"(+ 1 2)"

字符串就是用来表示一段文字的,两边双引号,里面的东西会全部当成文字,而不是程序。所以它会原样输出。一般只会用在给人看的一段文字中。

这样就讲完了,试验新的 abs 函数

> (abs #t)
; abs: number expected, given: #t

全部搞定。

再补充一下,= 这个函数只能判断数字相等,如果要其它类型,分别可以用 boolean=?string=?,没有 procedure=?,因为两个长得一模一样的函数是不相等的,原因见上一章。为了避免误导,就没有提供这个函数。

> (define f
    (λ (x) (+ x 1)))
> (define g
    (λ (x) (+ x 1)))
> (equal? f g)
#f

但有一个通用的函数 equal?,所有的类型它都可以比较是否相等。所以如果你们懒得去记各种函数的名字,就记个 equal? 就行了。一般用 =string=? 这些特殊的函数,只是为了强调参数的类型而已。对程序一般没多大影响。

这小节可能要记的东西比较多,但不用着急去记,暂时能看懂就行,慢慢就记住了。

练习题: 还记得上一章中的 add-what 函数吗,不记得的话回去翻一下 (传送门)。仿照上面的 abs 函数,检查 add-whatwhat 参数,确保它为 0~9 之间的整数。
注: 检查整数可用 integer?,小于或大于号也能有多个参数,比如 what 在 0~9 之间,可以写做 (<= 0 what 9)
请尽量写出让人能看明白的错误信息,并测试。

如果有如果的话

好了,现在是你在编程道路上的第一个难点。

好些时候,我们会需要重复做某些事,比如,又是上一章中的 add-what,一个 pie 两个 pie 差不多是够了,但对于 add-chip 来说,有没有一个函数可以一次性加上很多个 chip 呢?

(define add-many-chips
  (λ (num x)
    ....))
;; 在 x 上加上 num 个 chip

你的第一反应很重要。

(如果你数学真的很好的话,你也许会发现,num 个 1 就相当于 num 个 9 再除以 9,然后 num 个 9 等于 10num 再减 1,如果你以为你就这样成功了的话,那请你自己写一下乘方函数,然后你就遇到同样的问题了)

你可以慢慢尝试一下,

请暂停阅读。


你最后可能会发现,要是代码无限的话,只能这么写

(if (= 0 num) x ;; 什么都不加
    (if (= 1 num) (add-chip x) ;; 加1个
        (if (= 2 num) (add-chip (add-chip x)) ;; 加2个
            (if (= 3 num) .... ;; 加3个
                ....)))) ;; ......

接下来就是找规律时间。我们已经知道,num 是多少,就调用多少次 add-chip,但要把它向电脑表达清楚。

那么问题就是,什么叫做“调用 numadd-chip ”?

经过研究,我们发现我们没法把 numadd-chip 写出来,但是我们看看上面的那一堆 if,我们发现了规律:
如果 num 多了 1,那么 add-chip 也多调用 1 次。

有人说,这不是废话吗。但这就是“调用 num 次”的本质。

我先不多说,但你可以从现在开始思考一下,自然数是怎么来的。其实你小时候,数数的时候,就明白了,但现在又忘了而已。

数字就是这么数出来的。

最一开始,你还不会一次性跳到某个数。比如要你数一排苹果,你会用一只手点着苹果,一边计数:
“一个苹果”,然后指到下一个苹果,
“两个苹果”,然后指到下一个苹果,
“三个苹果”,……

如果 num 是 0 呢?add-chip 调用0次。
如果 num 比 0 多 1 呢?add-chip 多调用 1 次,就是 1 次。
如果 num 再比 1 多 1 呢?add-chip 再多调用 1 次,就是 2 次。
如果 num 再比 2 多 1 呢?add-chip 再多调用 1 次,就是 3 次。
……

于是对任意的 num 都可以数了。

程序把上面的过程反过来就可以了。

(define add-many-chips
  (λ (num x)
    (if (= 0 num)
        x
        (add-chip (add-many-chips
                   (- num 1) x)))))

你可能会感到有些惊奇,为什么 add-many-chips 里面还能调用它自己。前面做了那么多铺垫,其实就是为了讲这个。

首先,来理解一下这段程序。如果 num 为 0,就直接返回 x,什么都不加; 如果 num 大于 0,就先调用 add-many-chips 给它加上 num-1 个 1,然后再加一个 1。

你问我为什么 (add-many-chips (- num 1) x) 神奇般地就完成了?因为如果 num-1 是 0,那它就完成了,否则就继续减 1,一直重复,直到减到 0 了为止。

我们需要从程序的角度详细解释一下。首先,我们都说它是“自己调用自己”,其实函数是没有自己调用自己一说的。你可以看成是有无数个函数,就像这样

(define add-many-chips1
  (λ (num x)
    (if (= 0 num)
        x
        (add-chip (add-many-chips2
                   (- num 1) x)))))
(define add-many-chips2
  (λ (num x)
    (if (= 0 num)
        x
        (add-chip (add-many-chips3
                   (- num 1) x)))))
(define add-many-chips3
  (λ (num x)
    (if (= 0 num)
        x
        (add-chip (add-many-chips4
                   (- num 1) x)))))
......

有无数个这样的函数。比如调用 (add-many-chips 3 666)

> (add-many-chips1 3 666) ;; 每一行都是一步调用过程
=> (add-chip (add-many-chips2 2 666))
=> (add-chip (add-chip (add-many-chips3 1 666)))
=> (add-chip (add-chip (add-chip (add-many-chips4 0 666))))
=> (add-chip (add-chip (add-chip 666)))
=> (add-chip (add-chip 6661))
=> (add-chip 66611)
=> 666111

我认为这样你是可以理解的,这就是普通的函数调用罢了。

画图就看得比较清楚了。直到现在,我自己还是这么想象它的。

比如说我们调用了 (add-many-chips 3 666),首先,判断 num 是不是 0。(为了方便我竖着画)

add-many-chips-3-1.png

不是 0,所以先调用 add-many-chips,不过要记得完了还要加个 add-chip (有些箭头画不下了,省略了)

add-many-chips-3-2.png

然后我们就要开始计算 (add-many-chips 2 666),因为 2 不为 0,所以接着调用一层

add-many-chips-3-3.png

然后需要计算 (add-many-chips 1 666),同理,1 不为 0,再调用一层

add-many-chips-3-4.png

好了,现在最底下的这个函数终于发现它的参数是 0 了,于是它很高兴地返回了 666。

add-many-chips-3-5.png

接下来就明白了,这个 666 经过了 3 个 add-chip 最后得到了结果。

有人可能想问,那在最下面的函数计算的过程中,上面的那些函数干什么呢?

干等着。每一个函数都在等下面的那个函数返回结果,好继续自己的计算。这不是只有调用自己才会这样的,普通的函数也是这样的。

现在你应该大致能想象出这个流程了。我以前看书自学的时候,所有的教程都是一上来就说“自己调用自己”,这本来就是个误导。然后又开始讲各种高级的概念,比如“栈”,让我看得一头雾水。就这么个东西,我大概整了两年才搞懂。上面就是我现在自己的理解。

我现在不想提计算机到底是怎么实现函数一层层调用的,反正就可以当它是自动发生的。不过我们依然可以看出,每调用一层函数,它就会多占用一些内存,比如每层 add-many-chips 函数中都有自己的 numx 参数,当调用的非常非常多时,内存就存不下了。对于想知道术语的人: 这就叫做“栈溢出”。其它绝大多数语言里这个问题很普遍,因为栈是内存中固定的一部分。但 Racket 里就没有这玩意(除非你真的把电脑所有的内存都用完了),这也是我用 Racket 的一个原因,你可以专心地学习,不用担心函数调用的次数太多这类稀奇古怪的问题。

所以我们可以来玩一下。

> (add-many-chips 10000 666) ;; 相信我,我真的打了 10000 个 1 上去,不信你拖滚动条自己数数:p
6661111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

这在我的电脑上也是瞬间的事(DrRacket 中可能比较慢,是因为它把这个数字输出到屏幕上很慢,但开始输出的瞬间它就已经算出结果了)。电脑的速度真的比你想象中要快得多得多得多…所以我一直想知道,那些能让我的电脑变得很卡的软件究竟干了些什么…

思考题: (请先保存好你的代码,否则后果自负,不过除非你的操作系统抽风了,不然并不会影响系统和其它软件的,请放心)。给 add-many-chips 输入一个很大的数,内存可能就爆了,比如,我给了它一个亿…然后它 3 秒钟后占到了 3.2G 的内存然后卡住了…
好。这说明我们要量力而行,那给它一个小一点的数,比如 2.5 吧

> (add-many-chips 2.5 666)

为什么它又卡死了?

手动模拟。它要计算 1.5,然后 0.5,然后… -0.5,然后 -1.5,-2.5,…

同理,如果输入是负数的话也会死循环。

那就加点错误处理吧

;; 把原来的函数重命名为 add-many-chips-unsafe
;; 记得把函数里面调用自己的也重命名了
(define add-many-chips
  (λ (num x)
    (if (or (< num 0)
            (not (integer? num)))
        (error "add-many-chips: invalid first argument. expected: natural number, given:" num)
        (add-many-chips-unsafe num x))))

(define (or x y) ;; 只要参数有 #t 结果就为 #t,参数都是 #f 结果才为 #f
  (if x #t y))

当然,如果要详细地写,就还要先检查参数是否为数字,否则 (< num 0) 又会报错… 然后你就会被烦死。总之你永远也阻止不了别人错误地使用你的程序。

当然,你在用别人的程序时也一样,别人检查地再怎么仔细,如果你边打盹边打代码,就不要指望程序是正确的了。

以后我就很少再提错误检查了。因为代码这么短,动不动就检查反倒没必要。这节就这么结束了。

思考题: 有人可能认为没必要写两个函数,都写在一个函数里就可以

(define add-many-chips
  (λ (num x)
    (if (or (< num 0)
            (not (integer? num)))
        (error "add-many-chips: invalid first argument. expected: natural number, given:" num)
        (if (= 0 num)
                x
                (add-chip (add-many-chips
                           (- num 1) x))))))

第一,这段代码正确吗?

第二,如果正确的话,请手动模拟,比如调用 (add-many-chips 3 666),两个版本分别检查了多少次错误?效率上是否会有不同?

练习: 再来一瓶,再来一瓶

(警告: 这章会用到一点数学知识,因为我实在想不出不用数学的例子了, 如果有谁想出来了请一定要告诉我一声)

像上一节中所说的,函数调用自己的这种模式有一个名字,叫做“递归(recursion)”。

准确来说,函数除了直接调用自己,还可以间接调用自己,比如 f 调用了 gg 又调用 f

上一节中也看到了,递归跟普通的函数调用是一样的,没什么特殊的。唯一的区别就是它可能导致程序死循环。因为它相当于你写无数个函数。

下面是两个练习

第一个是上节提到的乘方函数。

(define expt ;; b 的 n 次方,只考虑 n 为自然数
  (λ (b n)
    (if (= 0 n)
        1
        (* b (expt b (- n 1))))))

如果你还不理解函数的原理,请复习上一节。但只知道这个函数是正确的还没有用,还需要学会自己写。

要写 bn,先来分析一下这个函数。首先,用自己能看懂的方式写出来

bn = b*b*…*b,共 n 个 b

然后改递归(当然,你要先确定没有更简单的方式了。比如写 1+2+…+n,当然就直接套求和公式了)

首先,考虑省略号中是什么?

就是 n 个 b,没错吧。而 b 就永远都是 b 这个定值,所以对 n 递归。

所以 n 的范围是什么?

我们只考虑自然数,所以是 0,1,2,3… 这些数字。

接下来就要找到一个方法,让我们不管输入什么 n,程序都不会死循环。然后用这个递归方法,把函数写出来。

对自然数来说,大多时候都是每次减 1,一直减到 0,根据情况有例外,下面就会举一个例子。

好,那就根据这个递归方案,试一下能不能写出程序。

b0 = 1,这种情况很简单。
若 n>0,那么 bn 要先调用 b(n-1),然后只要再乘个 b 就行了。
bn = b*b(n-1)

然后写成上面的程序就完成了。

繁琐的练习题:

  1. 如果把 b0 的情况改成 b3 = b*b*b 会怎么样?
  2. 如果把递归的情况写成 bn = b(n+1)/b 会怎么样?
  3. 如果把递归的情况写成 bn = b*b*b(n-2) 会怎么样?
  4. 把递归的情况写成第3题所示,怎样让函数总能停止?(提示: 两次 if)

练习题:
写出阶乘函数: factorial(n)=1*2*3*4*...*n , n>=1

根据上面的方法,我们要把 1*2*3*...*n 改成一个递归式,比如先尝试 1*(2*3*...*n),然后会发现失败了,因为 (2*3*...*n) 还是跟 n 有关,而不是跟 n-1 有关,所以会发现递归式应该是 n*(n-1*...*3*2*1)n*factorial(n-1)

这样代码就完成了

(define factorial
  (λ (n)
    (if (= 1 n)
        1
        (* n (factorial (- n 1))))))
;; test
> (factorial 10)
3628800

练习题:
写出函数 first-digit,输出一个正整数最高位的数字。

> (first-digit 123)
1

考虑一下。如果是个位数,就直接输出。这就是递归终止的情况。

(define first-digit
  (λ (n)
    (if (< n 10)
        n
        ....)))

否则,它就至少是两位数,把它的个位删掉,然后重复同样的过程。

(define single-digit? ;; 是否为一位数
  (λ (n)
    (if (< n 10) #t #f)))

(define delete-units-digit ;; 删掉个位数
  (λ (n)
    (quotient n 10))) ;; n/10 取整

(define first-digit
  (λ (n)
    (if (single-digit? n)
        n
        (first-digit (delete-units-digit n)))))

;; test
> (first-digit 123)
=> (first-digit (delete-units-digit 123))
=> (first-digit 12)
=> (first-digit (delete-units-digit 12))
=> (first-digit 1)
=> 1

总有一次,我们删到了只剩一位数,这个数就是原来最高位的数字。

这个函数虽然处理的是正整数,但它每次递归都调用 delete-units-digit,而不是把 n 每次减 1。而且它的终止条件也跟 n 有关,而不像前面的函数是一个常数。所以根据不同的目的,函数也有不同的变化,没有什么通用的模式可以用,但还是有规律可循,比如再写一个 count-digits 函数,数一数输入的数字是几位数。


下面给答案

(define count-digits
  (λ (n)
    (if (single-digit? n)
        1
        (+ 1 (count-digits
              (delete-units-digit n))))))

发现了吗,它跟 first-digit 函数很像,只有很小的改动。再之前的 exptfactorial 等函数也都很像。思考一下原因?

从它们的定义出发,如 expt 等函数就是要把自然数都数遍,所以采取每次减 1,但 count-digits 等函数不是从 1 到 n 一个数一个数算,而是 1 位数 1 位数的算,所以每次递归都是去掉个位数字。

最后练习题:
写出 sum-digits 函数,把每位数字加起来,比如

> (sum-digits 123)
6

建议:

(define get-units-digit ;; 取个位数字
  (λ (n)
    (remainder n 10))) ;; n/10 的余数

我们的 delete-units-digitget-units-digit 配成了一对。刚好把一个数的个位和其它位数拆开。有了这两个函数,你可以对一个数的每位数字随意做文章。不信?作为思考题,你要不试试怎么把一个数翻转?(reverse-digits 123) => 321

这已经跟下一章的内容有所关联了。因为我们现在好像已经不是在玩一个数了,我们现在在把一个数当成一串数字玩。一个“数字列表”。

一道有点出戏的思考题:
如果 if 不是个特殊语法,而是个函数的话,比如我们重新定义

(define my-if
  (λ (pred conseq alt)
    (if pred conseq alt)))

然后用这个 my-if 重新定义前面的递归函数,会发生什么?

这也是为什么 if 只能是一个特殊语法,而不是一个函数。否则,递归就停不下来啦。


开个玩笑,你去谷歌一下“recursion”

google-recursion.png

最后作为一个练习,如果你还不太理解递归,就把前面的函数画画图吧。

当我学到递归的时候,我晚上做的梦都是递归的,我天天都意识到我在梦里做梦。现在只能祝你今天有个好梦了。

跳入梦中

再给出上一节中的 add-many-chipsfactorial 函数。(上一节中的几个函数都是一样的道理)

(define add-many-chips
  (λ (num x)
    (if (= 0 num)
        x
        (add-chip (add-many-chips
                   (- num 1) x)))))

(define factorial
  (λ (n)
    (if (= 1 n)
        1
        (* n (factorial (- n 1))))))

下面是另一个 add-many-chips 的实现方式

(define add-many-chips
  (λ (num x)
    (if (= 0 num)
        x
        (add-many-chips (- num 1) ;; 这两行改变了
                        (add-chip x)))))

当然,先看懂它。

然后对比上一节中的图,我们画一下新版 (add-many-chips 3 666) 的图。(我就只画最终的情况了)

new-add-many-chips.png

你可能发现了什么,我们的函数被串成一条直线了,也就是说,下面的 add-many-chips 函数返回了以后就没有别的事了。这个结果可以直接拿去输出。

这有什么区别呢?在现在很多流行语言里,没有区别,但是在 Racket 里,前面说过了,如果给一个很大的输入,旧的版本会占非常大的内存,但你试一下新的版本?它居然几乎不占内存。不管多大的输入都一样 (除了我电脑的风扇转得飞快,CPU 飙升到 60 多度…)

这就要提一个新的概念了,仔细看新版的那幅图,你会发现,因为下面的那个函数的返回值可以直接输出,所以当我们往下递归一次时,上面的函数就已经都没有用了。所以 Racket 很机智地就把这些函数都扔掉了,只留了下面那一个有用的。名副其实的功利主义者啊。

(注: 你用 DrRacket 的话,右下角可以看它的内存,你会发现内存一会儿上升 一会儿又下降了,然后又慢慢上升…因为为了效率和简单性,Racket 不是一边计算一边扔没用的内存,而是过一段时间统一扔一次。这是一种计算机自动管理内存的方式,叫“垃圾回收(garbage collection)”,以后还会讲到。)

然后,由于这种递归是不会越来越占内存的,甚至比以前的递归方式效率还高,所以它专门有个名字,叫“尾递归(tail recursion)”。我现在不给详细的定义,但很显然,必须是要能把之前的东西全部扔掉的递归 才能叫尾递归,对吧。(我这里其实撒了个谎,尾递归也有可能越来越占内存,比如参数引用了应该被扔掉的东西的时候,但这个以后再说吧,现在这么理解是没问题的)

下面做个挑战,把 factorial 函数改成尾递归模式。

你会发现这个有些难度。首先,因为是尾递归,所以当然要给它加一个参数。然后…然后我还是直接给答案吧,自己体会

(define factorial
  (λ (n)
    (fact-iter n 1)))
(define fact-iter ;; iterate,可以理解为循环
  (λ (n acc) ;; acc: accumulate 累积的结果
    (if (= 1 n)
        acc
        (fact-iter (- n 1)
                   (* n acc)))))
;; test
> (factorial 4)
=> (fact-iter 4 1)
=> (fact-iter 3 4)
=> (fact-iter 2 12)
=> (fact-iter 1 24)
24

这个真的讲不来,只能靠你自己体会了。


(以下内容考虑删掉,初学者请略过,直接跳到下一条分割线)

拓展内容:

其实这个函数跟原来的 factorial 不是完全对应的,它利用了乘法的什么什么律。原来是从1乘到n,现在是从n乘到1。要跟原来计算顺序对应,可以这么写

(define factorial
  (λ (n)
    (fact-iter n 1 1)))
(define fact-iter
  (λ (n counter acc)
    (if (= n counter)
        acc
        (fact-iter n
                   (+ 1 counter)
                   (* counter acc)))))

为什么要提这个呢?因为我们不用任何特殊的定律就可以转换,也就是说,我们有固定的方案转换这种递归!大致就是这样

(define f
  (λ (x)
    (if (= stop x)
        stop-value
        (g x (f (h x))))))
=>
(define f
  (λ (x)
    (f-iter x stop stop-value)))
(define f-iter
  (λ (x counter acc)
    (if (= x counter)
        acc
        (f-iter x (h' counter) (g (h' counter) acc)))))

其中 h'h 的反函数,比如 h(x)=x-1,那么 h'(x)=x+1g 可以是任意函数。

比如我们举个例子吧,取 h(x)=x-1stop=0 简单理解一下

;; 非尾递归
> (f 3)
=> (g 3 (f 2))
=> (g 3 (g 2 (f 1)))
=> (g 3 (g 2 (g 1 (f 0))))
=> (g 3 (g 2 (g 1 stop-value)))

;; 尾递归
> (f 3)
=> (f-iter 3 0 stop-value)
=> (f-iter 3 1 (g 1 stop-value))
=> (f-iter 3 2 (g 2 (g 1 stop-value)))
=> (f-iter 3 3 (g 3 (g 2 (g 1 stop-value))))
=> (g 3 (g 2 (g 1 stop-value)))

考虑删掉的内容到此结束


练习题: 照着上面的 factorial 函数,把 expt 函数也转换成这种形式。


答案:

(define expt
  (λ (b n)
    (expt-iter b n 1)))
(define expt-iter
  (λ (b n acc)
    (if (= 0 n)
        acc
        (expt-iter b
                   (- n 1)
                   (* b acc)))))

下面的习题会告诉你自然数是怎么定义出来的。

首先,我们有一个 0,然后,我们有一个函数 add1,它能把一个数加 1。这是我们仅有的两个东西(当然,我们现在还不知道“两”是什么意思:)。

好了,接下来所有的自然数我们都有了,就像这样 (当然不要输到电脑里,因为数字不能做变量名,只是让你意会一下)

(define 1 (add1 0))
(define 2 (add1 1))
....
....

然后我们来定义加减乘除吧。

首先的难点就是,如何定义一个减 1 的函数 sub1

方法就是从 0 加上去,如果发现某个数加 1 以后等于参数,那么返回这个数

(define sub1
  (λ (x)
    (if (= 0 x)
        (error "sub1: invalid argument. expected: positive integer, given: 0")
        (sub1-iter x 0))))
(define sub1-iter
  (λ (x result)
    (if (= x (add1 result))
        result
        (sub1-iter x (add1 result)))))

然后定义加法

(define +
  (λ (a b)
    (if (= 0 a)
        b
        (add1 (+ (sub1 a) b)))))

;; 尾递归法
(define +
  (λ (a b)
    (if (= 0 a)
        b
        (+ (sub1 a) (add1 b)))))

练习题: 照同样的方式定义减法、乘法、除法,除法会略麻烦,因为有除不尽的情况,你可以选择直接向下取整。

下面两道练习题的口味有点重,看到数学就烦的人请略过。

练习题: 写出函数 prime?,判断一个数是否为质数 (方法: 从小到大找,如果找到一个因数((= 0 (remainder ? ?))) 就说明不是质数,如果一直找到足够大(自己思考是多大)都没找到,说明是质数)

练习题: 上一章中的倒数第二节的最后我们实现了一个开平方根的函数,现在修改它,让它最终精确到某个精确度而不是重复几次。用很小的小数和很大的数测试,确保都能得出适当的结果。因为计算机只能保存有限位小数,如果实现不恰当,有可能太大或太小的数就会陷入死循环。判断相邻两次递归结果不变时终止也不一定可行,因为可能最后几位小数一直来回摆动。总之自己去试吧。

这几题还是有点难度的,我不想给答案了,当时我都是自己写出来的,你们应该也有能力完成的。写出来以后记得稍微测试一下,但不要依赖于测试,在测试之前就要确保它是对的。(是不是听起来很矛盾)

好了,数学部分终于结束了

私有财产

回顾一下到现在为止函数的作用有哪些:
把一段程序的细节隐藏起来,提取重复的代码,和 define 结合起来可以递归。

是的,λ 还有跟 define 一样的作用,就是创造新的变量名字。

比如这样

> ((λ (x) (* x x))
   (+ 1 2))

这样也实现了 (+ 1 2) 的平方,应该没有疑问了吧。

它的用处在哪里呢?在函数里,或者 if 的分支中,即使你现在没想过,迟早你会想要定义一些临时的变量或函数的。一个很直接的想法就是,比如尾递归式的 factorial 等函数,不要让它们的 iter 函数暴露在外面。

其实 Racket 本身就提供了,不用辛苦地去套一层 λ 了。一个 λ 函数内可以先有一些 define 语句,最后跟着输出,比如

(define sum+sqr
  (λ (x y) ;; x+y 的平方
    (define a (+ x y))
    (* a a)))
;; 当然更推荐的是
(define sum+sqr
  (λ (x y)
    (sqr (+ x y))))

我们也可以改一下我们写过的那些函数。

(define factorial
  (λ (n)
    (define fact-iter
      (λ (n acc)
        (if (= 1 n)
            acc
            (fact-iter (- n 1)
                       (* n acc)))))
    (fact-iter n 1)))

这里,我们就是简单地把 fact-iter 的定义拿进来了,所以除了 factorial 这个函数,别的人就看不到它了。你需要先回忆一下上一章的内容,理解一下里面的 n 都是哪个 n,DrRacket 做得很好,你把鼠标移到变量上,它就会显示出这个变量来自哪里,还会把跟它相同的变量都加亮,跟它不同的不会变色。

drracket-example.png

提醒一个常见的错误,可能有人想要定义一个函数,它能帮他定义很多东西,比如:

(define f
  (λ ()
    (define a 1)
    "define a"))
> (f)
"define a"
> a
; a: undefined;
;  cannot reference an identifier before its definition

虽然 a 是在 f 中定义的,但它的生命跟 f 的参数是一样短的,在 f 结束后就一起消失了(还不为它默哀?你们这些写出这段代码的人)。有想要这么干的人,请去网上搜“Racket 宏(macro)”,不保证你们能看懂。


也许你会觉得把一个函数挪进来没什么用,但反正作为一个好习惯,因为 fact-iter 也是 factorial 的实现细节,所以就不应该让别人看到,就先这么理解着吧。

对于一些函数来说,能够简化代码,比如

(define expt
  (λ (b n)
    (define expt-iter
      (λ (n acc)
        (if (= 0 n)
            acc
            (expt-iter (- n 1)
                       (* b acc)))))
    (expt-iter n 1)))

跟前面的 expt-iter 对比一下,你会发现我们可以省略一个参数 b,因为它一直是不变的,只是传来传去而已。

练习题: 同样的,把之前的 sqrt 函数用到的所有的函数都拿进来,尽可能地减少函数的参数。(原来的 f 有一个 a 一直传,现在可以消灭掉了)

注: 从这里开始一直到这章结尾的内容也考虑去掉

另外,因为接在 if 两个分支处要定义变量的情况非常普遍,有好几重 if 判断的情况也很普遍,Racket 还提供了一个语法叫 cond

(define abs
  (λ (x)
    (cond
      [(>= x 0) x]
      [else (- x)])))

(define prime?
  (λ (x)
    (define divisor?
      (λ (a b)
        (integer? (/ b a))))
    (define search
      (λ (n)
        (cond
          [(> n (sqrt x)) #t]
          [(divisor? n x) #f]
          [else (search (+ 1 n))])))
    (search 2)))

;; test
> (prime? 19260817)
#t
> (prime? 10000000)
#f
> (prime? 2)
#t
;; 我写完这种代码都不敢说一定是对的,一定要测试一下,
;; 不然写博客被打脸了怎么办

cond 的语法大致是这样

(cond
  [?1 v1]
  [?2 v2]
  [?3 v3]
  ....
  [else v-else])
;; ?n 和 vn 都可以是任意表达式

;; 若 ?1 则 v1,否则
;; 若 ?2 则 v2,否则
;; ....
;; 最终,如果上面都不满足,值为 v-else
;; else 这一行也可以省略,会默认变成一个报错语句:
;;      没有满足的条件

;; 也可以跟多个表达式
(cond
  [?1 def1 def2 .... defn v1]
  ....)
;; 其中 ?1 是判断条件,
;; def1 ~ defn 是 define 语句
;; v1 是最终的值

(注: 其实在 Racket 中,方括号和圆括号是不分的,只要它们能配对就行,但作为好习惯,这些函数调用和提供的语法都用圆括号,像 cond 中这些不表示一个“动作”的东西就用方括号,这样看起来会清楚得多)

这个语法可能有点麻烦。一般来说,语法都是有统一的格式的,记住一个以后,类似的就都记住了。

这就是所有的内容了,这章目前只是草稿,还有待修改和完善,另外如果有谁想出了更好的题目欢迎联系我。