Scheme笔记

scheme笔记

几个概念 concept

identifier

标识符,可以看作是一个唯一的名字,可以用来引用变量,函数等等

variable

变量

atom

  • a string of characters
  • a string of digits
1
2
3
4
(define atom?
(lambda (s)
(and (not (pair? s))
(not (null? s)))))

list

a collection of atoms enclosed by parentheses. example:

1
2
3
4
'()
'(a)
'(a b c)
'((a b) c d)

语法描述:

1
2
list -> '()
| (sexp . list)

S-expression

  • all lists
  • all atoms
1
2
sexp -> list
| atom

Naming Conventions

  1. 谓词后加? , 但是常用的数字比较=, <, >, <=, >=后面不需要?
  2. 类型测试, pair? , atom?
  3. 字符操作(char-xxx), 字符串操作(string-xxx), 向量操作(vector-xxx)
  4. 类型转换(type1->type2)
  5. 但函数有副作用时,应该以 ! 结尾, 比如 set!

core syntactic forms core syntactic form

  1. top-level define forms 实际就是创建新的绑定,可以绑定 list, lambda procedure
    这是几个示例代 码:

    1
    2
    3
    4
    5
    6
    7
    8
    (define double-any
    (lambda (f x)
    (f x x)))
    ;;; identical
    (define (double-any f x)
    (f x x))

    (define xyz '(x y z))
  2. constants BNF:

    1
    2
    3
    4
    <constant> -> <boolean>
    | <number>
    | <character>
    | <string>
  3. variables

  4. procedure applications (procedure arg1 ... argn) 求值规则:

    1. Find the value of procedure.
    2. Find the value of arg1.
    3. Find the value of argn.
    4. Apply the value of procedure to the values of arg1argn.
  5. quote expressions (‘)

    1. quoting an identifier tells Scheme to treat the identifier as
      a symbol rather than as a variable. 也就是说scheme解释器不会去当前的
      环境中寻找与该标识符绑定的值, 而是直接将该标识符当作symbol也就是数据处理.
    2. quoting a list tells scheme to treat the list as data, rather
      than as a procedure application
  6. lambda expressions (lambda (x) (+ x x)) ==> #<procedure>

    一般形式有这三种:

    • (lambda (var1 var2 ...) exp1 exp2 ...): var1 var2 …会依次赋值
    • (lambda (var1 var2 . var) exp1 exp2): var1 var2会依次赋值, 余下的参数
      会组成一个列表赋给var
    • (lambda var exp1 exp2): 将所有的实参作为一个list赋给var, 注意var没有括号
  7. if expressions: (if test-expr then-expr else-expr) 只有
    test-expr#f 时才会运行 else-expr, 所以你一般要使用 null? eq? 这样的函数
    来测试

  8. set! expressions(Assignment): Assignments do not create new
    bindings, as with let or lambda, but rather change the values of
    existing bindings.也就是说赋值不会像let, lambda那样产生新的绑定,而是会改变已存在绑
    定的值,如果你给set!指定的符号不存在,它会报错(set!: cannot set undefined
    variable)

其它的都是一些扩展,也就是可以通过define-syntax定义出来的,比如let,and,or,not等
等,只有以上的部分才需要解释器直接实现的

1
2
3
4
5
6
7
8
9
10
11
(define-syntax let
(syntax-rules ()
[(_ ((x v) ...) e1 e2 ...)
((lambda (x ...) e1 e2 ...) v ...)]))

(define-syntax and
(syntax-rules ()
[(_) #t]
[(_ e) e]
[(_ e1 e2 e3 ...)
(if e1 (and e2 e3 ...) #f)]))

实际上define-syntax实际就是进行模式匹配,像cond,如果匹配那么就进行替换,比 如(and 1)就匹配((_ e)
e)所以就被替换为1, 就是and的占位符, 而 pat ... 代表 0个或者多个表达式, 比如
(x v) ... 代表有0个或多个 (x v) 这样的表达式

let let

1
2
3
4
5
6
7
8
9
10
;;; form 1
(let ((a exp1)
(b exp2))
(body1)
(body2))
;;; form2
(let f ([a exp1]
[b exp2])
(body1)
(body2))

由于 scheme 对待中括号与对待小括号是一样的,所以为了可读性, let 一般可以用如下代 码:

1
2
3
4
(let ([a exp1]
[b exp2])
(body1)
(body2))

form2主要是为了递归, 它使得在函数体中可以引用函数名, 比如下面的代码

1
2
3
4
(let fac ([n 10])
(if (zero? n)
1
(* n (fac (sub1 n)))))
  • let: 如上所见,实际就是一个扩展语法,a,b只在body中可见, 所以你在exp2中不能使 用a

  • let*: exp2中可以引用a, 也就是a可以用来定义b, 可以用嵌套的 let 来定义 let*

    1
    2
    3
    4
    5
    6
    7
    (define-syntax let*
    (syntax-rules ()
    [(_ () e1 e2 ...)
    (let () e1 e2 ...)]
    [(_ ((x1 v1) (x2 v2) ...) e1 e2 ...)
    (let ((x1 v1))
    (let* ((x2 v2) ...) e1 e2 ...))]))
  • letrec: 主要用来解决定义递归函数时,函数名在函数体中不可见的问题,比如

    1
    2
    3
    4
    (let ([sum (lambda (lst)
    (if (null? lst) 0
    (+ (car lst) (sum (cdr lst)))))])
    (sum '(1 2 3 4 5)))

    如果运行上面的代码,那么你会 sum undefined 的错误, 原因是 sum 在后面的函数 体中不可见, 所以你使用
    (sum (cdr lst)) 就会出错,当然你可以使用这种方法

    1
    2
    3
    4
    (let ([sum (lambda (sum lst)
    (if (null? lst) 0
    (+ (car lst) (sum sum (cdr lst)))))])
    (sum sum '(1 2 3 4 5)))

    但这种方法不够自然,而且比较丑陋,不符合scheme中定义递归函数的一般模式,所以 就引入了 letrec.

    1
    2
    3
    4
    5
    6
    (letrec ([sum (lambda (ls)
    (if (null? ls)
    0
    (+ (car ls) (sum (cdr ls)))))])
    (sum '(1 2 3 4 5)))
    ;; the result is 15
  • letrec*: 和 letrec 类似, 只是后面的绑定可以引用前面已经绑定的变量

  • let-values: 绑定多个变量 syntax:

    • (let-values ((formals expr) …) body1 body2 …)
    • (let*-values ((formals expr) …) body1 body2 …)
    1
    2
    3
    4
    5
    (let-values ([(a b) (values 1 2)] [c (values 1 2 3)])
    (list a b c)) ; the result is (1 2 (1 2 3))

    (let*-values ([(a b) (values 1 2)] [(a b) (values b a)])
    (list a b)) ; the result is (2 1)

control flow

if if

当要写多个表达式时应该加入 begin , 注意 begin 会依次执行它所包含的表达式,并 返回最后一个表达式的值

1
2
3
4
5
(if (test-exp)
(begin
expression1
expression2)
expression3)

如果 test-exp 为true, 依次执行 expression1, expression2 那么返回
expression2 的值, 之所以需要 begin 是由 if 的语法决定的, if 的语 法如下:

1
2
3
(if (test-exp)
(true-exp)
(false-exp))

所以如果你在 true-exp 的位置放入多条表达式,那么这些表达式的第二条会 被当作 false-exp ,而且因为 if
后的表达式的条数超出 2 条而报错, 所以 你需要把多条表达式括起来,但是你不能直接加一个括号,比如

1
((true-exp1)(true-exp2))

这样之所以不行是因为scheme会将 (true-exp1) 当作procedure求值,这显然不 对,
所以scheme使用begin(更准确的说begin是为了引入side effect), 也就变成 了:

1
2
3
(begin
(true-exp1)
(true-exp2))

这就是if使用begin的原因了, 注意在 let, lambda, define,cond的body中都不需
要begin,因为它们都没有if这种特殊的状况.

cond cond

内部相当于有个隐含的begin,所以可以直接写多个表达式

1
2
(cond [(test-exp) (exp1) (exp2)]
[else expression3])

如果test-exp为true, 那么返回expression2的值 注意一个特殊的形式: =>

1
2
(cond ((assv 'b '((a 1) (b 2))) => cadr)
(else #f)) ;; ==> 2

会将 test-exp的值传递给 =>后面的函数, =>后面必须是一个带一个参数的函数

when unless when unless

  • (when (test-exp) exp1 exp2) : test-exp为真就执行exp1, exp2

    1
    2
    3
    (define-syntax when
    (syntax-rules ()
    [(_ e0 e1 e2 ...) (if e0 (begin e1 e2 ...))]))
  • (unless (test-exp) exp1 exp2) : 只有当 test-exp 为#f时才会运行body中的 表达式

case case

类似于C语言的switch

1
2
3
4
5
(let ((x 4) (y 5))
(case (+ x y)
((1 3 5 7 9) 'odd)
((0 2 4 6 8) 'even)
(else 'out-of-range))) ;; ===> odd

do do

syntax: (do ((var init update) ...) (test result ...) expr ...)

循环(do ((var init update) …) (test res …) exp …) var的初始值是init,接
着每一次迭代都绑定到update, (test res..)如果为true,那么就终止循环

1
2
3
4
5
6
7
8
(define divisors
(lambda (n)
(do ((i 2 (+ i 1))
(ls '()
(if (integer? (/ n i))
(cons i ls)
ls)))
((>= i n) ls))))

map

(map procedure list1 list2 …) 会返回一个 list

1
2
3
(map (lambda (x y) (* x y))
'(1 2 3 4)
'(8 7 6 5)) ; return (8 14 18 20)

mapfor-each 可以代替许多循环的工作, 而且逻辑上比循环更清晰.

for-each

map 类似但是不返回一个list作为结果, 也就是说 for-each 是用来产生side effect.

apply

(apply procedure obj … list)

  • (apply + '(4 5)) ===> 9
  • (apply min 5 1 3 '(6 8 3 2 5)) 结果是1

multiple values

  • (values obj …) :返回多个值,注意它的返回值可以用 let-values 来绑定,不能用 define

    1
    2
    (let-values ([(a b) (values 1 2)])
    (+ a b))
  • (call-with-values producer consumer): producer产生多个值, 然后将这些值传
    递给consumer.注意producer必须可以不带参数的方式来调用

    1
    2
    3
    (call-with-values
    (lambda () (values 'bond 'james))
    (lambda (x y) (cons y x))) ; the result is (james . bond)

内置的函数

predication

  1. = : 只用来比较整数, 不要用来比较浮点数
  2. eq? : 可以类似的看作是指针比较, 即便是内容相同,但是如果是两个不同对象,它 就返回 #f, 比如 (eq? (cons 'a 'b) (cons 'a 'b)) 虽然是同样的list, 可 是它在内存中的位置不同,所以为 #f.
    有以下几种情况是相等的.
    • #t, #f, 两个identifier的值如果都是#t或者#f,那么它们相等

    • 相同的符号(symbol), 比如

      1
      2
      3
      (define sym1 'hello)
      (define sym2 'hello)
      (eq? sym1 sym2)
    • ‘()

  3. eqv? : 和 eq? 很类似, 只是它在一些 eq? 没有定义的地方也可以使用
  4. equal? : 只要内容相同,它就会返回 #t,可以认为它的检查比 eq? 宽松
  5. boolean? : 等价于 (lambda (x) (or (eq? x #t) (eq? x #f)))
  6. null? : 只作用于list, only (null? '()) return #t.
  7. pair? :
  8. number? :
  9. complex? :
  10. rational? :
  11. real? :
  12. integer? :
  13. char? :
  14. string? :
  15. vector? :
  16. symbol? :
  17. procedure? :
  18. bytevector? :
  19. hashtable? :

list procedure

list 的语法定义:

1
2
List ::= '()
::= (Sexp . List)
  1. cons: (cons ‘a ‘b) ==> (a . b),只能带两个参数

  2. cons* : (cons* ‘a ‘b ‘c) ==> (a b . c) (cons* ‘a ‘b ‘(c)) ==>
    (a b c)

  3. car: the firsts element of pair (only for non-empty list )

  4. cdr: Only for non-empty list, the cdr of any non-empty list is also
    a list.

  5. set-car! :有副作用, 会原地改变pair的值

  6. set-cdr! :

  7. car, caar, caaar…etc: 连续执行n次(a的次数)car (caar ‘((5)) ) ===> 5

  8. cdr cddr cdddr …etc : 连续执行n次(d的个数)cdr

  9. cadar : (car (cdr (car lst))) 记住按照顺序从左到右,最右边的先对list起作用

  10. list: 创建一个list eg: (list 1 2 3 4) ===> ‘(1 2 3 4)

  11. length: list的长度

  12. append: 将一个list添加另一个list的后边,eg: (append ‘(1 2) ‘(3 4)) ===> ‘(1 2 3

  13. reverse: 将list倒转

  14. (list-ref list n): 第n个元素

  15. (list-tail list n) : 倒数第n个元素

  16. memq memv member memp 分别用eq? eqv? equal? 指定的procedure 来测试一个元
    素是否属于list, 如果属于那么就返回包括该元素以及该元素后面的元素组成的 list, (memq 'a '(b c a d e)) --> (a d e)

    1
    2
    3
    4
    5
    6
    (define memq
    (lambda (x ls)
    (cond
    ((null? ls) #f)
    ((eq? (car ls) x) ls)
    (else (memq x (cdr ls))))))
  17. remq (obj list) remv remove remp 删除list中的所有obj

  18. assq assv assoc assp 可以看做是关联数组((key1 . val1) (key2 . val2) …)

    1
    2
    3
    4
    5
    6
    (define assq
    (lambda (x ls)
    (cond
    ((null? ls) #f)
    ((eq? (caar ls) x) (car ls))
    (else (assq x (cdr ls))))))

    返回第一个匹配的pair

  19. (filter procedure list): return a list of the elements of list for
    which procedure returns true

  20. (partition procedure list): 返回两个list,第一个list包含所以使 procedure
    为#f的元素,第二个包含所有使 procedure#f 的元素, partition
    的返回值可以使用 let-values 来绑定或者使用 call-with-values
    来调用其它函数

  21. (find procedure list) : 返回第一个使procedure为#t的元素

  22. (map f list1 list2 …)

  23. (foldl f init list1 list2 …): racket 的内置版本有bug

  24. (foldr f init list1 list2 …):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    (define foldl
    (lambda (f x ls)
    (cond
    [(null? ls) x]
    [else
    (foldl f (f x (car ls)) (cdr ls))])))
    ;; (foldl op init '(1 2 3 4))
    ;; we can think foldl's behavior like this: (init op 1 op 2 op 3 op 4)
    ;; op has left associativity
    (define foldr
    (lambda (f x ls)
    (cond
    [(null? ls) x]
    [else
    (f (car ls) (foldr f x (cdr ls)))])))
    ;; (foldr op init '(1 2 3 4 5))
    ;; we can think foldl's behavior like this: (1 op 2 op 3 op 4 op init)
    ;; op has right associativity

number procedure

  1. zero? :
  2. positive? :
  3. negative? :
  4. even? :
  5. odd? : 奇数
  6. 数学函数: max, min, floor, ceiling(向+无穷取整),
    truncate(向0取整),round(最接近的整数),
    abs,gcd,expt(指数),三角系列函数(san,cos….)

char

字符使用 #\x 来表示,比如 a 就是 #\a,

  • (char=? char1 char2 char3 …)

  • (char<? char1 char2 char3 …)

  • (char>? char1 char2 char3 …)

  • (char<=? char1 char2 char3 …)

  • (char>=? char1 char2 char3 …)

  • (char-ci=? char1 char2 char3 …) 大小写敏感

  • (char-ci<? char1 char2 char3 …)

  • (char-ci>? char1 char2 char3 …)

  • (char-ci<=? char1 char2 char3 …)

  • (char-ci>=? char1 char2 char3 …)

  • (char-upcase char) :大写,相对应的还有个char-downcase

  • (char->integer char)

  • (integer->char n)

string

双引号内的是字符串

  • (string=? char1 char2 char3 …)
  • (string<? char1 char2 char3 …)
  • (string>? char1 char2 char3 …)
  • (string<=? char1 char2 char3 …)
  • (string>=? char1 char2 char3 …)
  • (string-ci=? char1 char2 char3 …) 大小写敏感
  • (string-ci<? char1 char2 char3 …)
  • (string-ci>? char1 char2 char3 …)
  • (string-ci<=? char1 char2 char3 …)
  • (string-ci>=? char1 char2 char3 …)
  • (string char …): 构建个包含指定的字符的字符串
  • (make-string n), (make-string n char)
  • (string-append string …)
  • (substring string start end)
  • (string-upcase string)
  • (string->list): 把string转换成包含char的list
  • (char->list lst)

vector procedure

list访问时需要遍历,不够高效, vector可以像数组一样O(1)时间内访问

  1. (vector obj …)
  2. make-vector: (make-vector 5 ‘a) ===> #(a a a a a)
  3. vector-length: vector的长度
  4. (vector-ref vec n)
  5. (vector-set! vec n obj)
  6. (vector-fill vec obj): 所有的元素都替换为obj
  7. list->vector :
  8. vector->list :

symbol procedure

每一个symbol在解释器内部都是指向同一个对象,所以用eq?来测试相同的symbol会返 回#t, 这也使得比较操作很高效

  1. symbol=? :symbol是否相等,也可以用eq?来比较
  2. symbol->string:
  3. string->symbol:

Hash Table

  • (make-eq-hashtable) : 使用eq?来比较两个key, 返回一个hashtable

  • (make-eq-hashtable size)

  • (make-eqv-hashtable) : 使用eqv?来比较两个key

  • (make-eqv-hashtable size)

  • (make-hashtable hash equiv?) : hash指定hash函数, equiv?指定比较两个key的 函数

  • (make-hashtable hash equiv? size)

  • (hashtable-set! hashtable key obj) :

  • (hashtable-ref hashtable key default) :

  • (hashtable-delete! hashtable key) :

  • (hashtable-size hashtable) : hashtable的大小

  • (hashtable-contains? hashtable key) :测试是否包含指定的key

macros

定义一个宏实际上就是把一个keyword与一个 macro transformer 绑定, 一个macro
transformer一般就是一个带有一个参数的函数, 而macro transformer的输入是一个原
始代码的syntax object, 输出则是包含转换后代码的 syntax object. syntax-rules
返回的就是一个macro transform.所以你可以抛开 syntax-rules 而这样写一个宏:

1
2
3
4
(define-syntax self-as-string
(lambda (stx)
(datum->syntax stx
(format "~s" (syntax->datum stx)))))

define-syntax

和define类似

1
2
3
4
5
6
(define-syntax let*
(syntax-rules ()
[(_ () b1 b2 ...) (let () b1 b2 ...)]
[(_ ((i1 e1) (i2 e2) ...) b1 b2 ...)
(let ([i1 e1])
(let* ([i2 e2] ...) b1 b2 ...))]))

let-syntax与letrec-syntax

1
2
3
4
5
6
7
8
9
10
11
12
13
(let ([f (lambda (x) (+ x 1))])
(let-syntax ([f (syntax-rules ()
[(_ x) x])]
[g (syntax-rules ()
[(_ x) (f x)])])
(list (f 1) (g 1)))) ;; (1 2)

(let ([f (lambda (x) (+ x 1))])
(letrec-syntax ([f (syntax-rules ()
[(_ x) x])]
[g (syntax-rules ()
[(_ x) (f x)])])
(list (f 1) (g 1)))) ;; (1 1)

syntax-rules

基本语法: (syntax-rules (literal ...) (pattern template) ...)

返回一个transformer, 实际就是一个 procedure, 这个 procedure 接受一个 syntax
object,然后返回一个syntax object.

  • literal: 一些关键字, 出现在pattern会原样匹配
  • pattern: 用于匹配输入的表达式的模式
  • template: 输出,记住template是原样输出,这是和syntax-case的最大区别.
  • 下划线’_‘: 可以匹配任何结构,一般用来代表宏名
  • … : 代表前面的部分重复0次或者多次

一个例子,这个例子实现一个类 if 的条件判断语句,他的形式是 (my-if cond then exp1 else exp2) 这看起来更可读性更好:

1
2
3
4
(define-syntax my-if
(syntax-rules (then else)
[(_ condition then true-exp else false-exp)
(if condition true-exp false-exp)]))

then, else 是关键字,所以它会原样的匹配输入

syntax object

(syntax template) 的缩写为 #'template, syntax和quote很类似,只是它会将
template中的pattern variable替换掉, 并且会绑定上下文信息

  1. syntax: 创建一个字面的 syntax object,比如 (syntax '(+ 1 x)), 简写 #'(+ 1 x).
  2. syntax->datum: 将一个syntax object转换为它原来的内容.
  3. identifier? : 测试一个syntax object 是不是标识符
  4. syntax-e : 只解包一层.

syntax-case

syntax-rules 更具一般性,而且更通用, 使用syntax-case的一般形式是:

1
2
3
4
5
6
7
8
9
10
(define-syntax macro-name
(lambda (x)
(syntax-case x (other keywords go here if any)
[
(macro-name macro-arg1 macro-arg2)
;;Expansion of macro (one or multiple forms)
;;(syntax is a reserved word)
(syntax (expansion of macro goes here))
] ...
)))

解释一下上面的代码: 整个lambda定义的就是一个macro transformer, 它的唯一参数 x
实际就是一个包含了原始代码的syntax object. 通过syntax-case
来匹配x,然后返 回一个包含了转换后代码的syntax object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(define-syntax my-if
(lambda (x)
(syntax-case x (then else)
[(_ condition then true-exp else false-exp)
(syntax (if condition true-exp false-exp))])))
;;Define a new macro
(define-syntax swap!
(lambda (x)
;;we don't have any keywords this time
(syntax-case x ()
[(swap! a b)
(syntax
(let ((c a))
(set! a b)
(set! b c)))]
)))

(define-syntax syntax-rules
(lambda (x)
(syntax-case x ()
[(_ (i ...) ((keyword . pattern) template) ...)
#'(lambda (x)
(syntax-case x (i ...)
[(_ . pattern) #'template] ...))])))

with-syntax

1
2
3
4
5
6
(define-syntax with-syntax
(lambda (x)
(syntax-case x ()
[(_ ((p e) ...) b1 b2 ...)
#'(syntax-case (list e ...) ()
[(p ...) (let () b1 b2 ...)])])))

input and output

port

scheme的输入输出用到一个叫做port的对象,port也是first class object, port有很
多种类,比如文件,字符缓冲区, tcp网络连接, 管道等等

  1. 文件:
    • (open-output-file fname) : 返回一个port,用于将内容写入到文件

    • (open-input-file fname) : 返回一个port用于读取指定文件的内容

    • (close-output-port port)

    • (close-input-port port)
      还有一种常用方式(更简单, 会自动关闭port):

    • (call-with-input-file filename proc) : 打开filename并将得到的port传递
      给proc, 并且调用完成时会关闭port

    • (call-with-output-file filename proc) : 打开filename并将得到的port传递
      给proc, 并且调用完成时会关闭port

      1
      2
      3
      4
      5
      6
      7
      8
      (call-with-output-file "data"
      #:exists 'truncate
      (lambda (out)
      (display "hello" out)))

      (call-with-input-file "data"
      (lambda (in)
      (read-line in)))
  2. 字符缓冲区
    • (open-output-string) :
    • (open-input-string “xxx”):
  1. default port

    默认解释器会打开2个port, 一个输入,一个输出,分别会绑定到shell的输入输出,很 多实现还会打开一个error
    port,用于IO的scheme procedure都可以带一个可选的参 数port, 如果不指定port,
    那么就使用默认的port, 默认的port有这两个函数获得

    • (current-input-port)
    • (current-output-port)

    如果要改变默认的port,可以使用下面的两个函数:

    • (with-input-from-file filename thunk) : 将默认的 input port重新绑定到文
      件,这可以实现重定向
    • (with-output-from-file filename thunk) : 将默认的 output port重新绑定到
      文件,这可以实现重定向

输入输出

  • 输入:
    1. read: 它会自动将读入的内容转换为scheme内置的数据结构,实际是一个递归下 降的parser
    2. (read-char), (read-char input-port): next character
  • 输出:
    1. print
    2. write
    3. display
    4. printf: 可以指定一个字符串来格式化,用的较多
    5. fprintf: 写入文件,多一个port参数

code example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(call-with-input-file "myfile.ss"
(lambda (p)
(let f ((x (read p)))
(if (eof-object? x)
'()
(cons x (f (read p)))))))

(define read-word
(lambda (p)
(list->string
(let f ()
(let ((c (peek-char p)))
(cond
((eof-object? c) '())
((char-alphabetic? c)
(read-char p)
(cons c (f)))
(else '())))))))

递归思想

递归的思想要从逻辑上理解,在设计一个递归函数时,一开始就要明确该函数在逻辑上
的作用(不要一开始就陷入编码细节中),然后分清该函数在逻辑上与子问题的联系,并
以此为 依据来进行函数体的设计, 特别需要注意检查结束条件,比如加法(0), 乘法 (1), list(‘())就是一些结束条件.

递归函数的设计要点:

  1. 要明确检查终止条件(null? zero?)
  2. 要明确与子问题的关系,弄清分类的情况(cond)

continuation

scheme中获得当前continuation的构造是call/cc, let/cc

1
2
3
4
5
(let/cc k                               ;k is the continuation
body)
;;; identical
(call/cc (lambda (k) ;k is the continuation
body))

停机问题

numberLines
1
2
3
4
(define last-try
(lambda ( x )
(and ( will-stop ? last-try)
( eternity x ))))

上述代码中:eternity会永远运行,比如:

1
2
3
(define eternity
(lambda (x)
(eternity x)))

假设存在停机函数 will-stop?, will-stop? 可以测试出一个函数是否会停机.那么:

  1. 假设 last-try 会停机, 那么 (will-stop? last-try) 返回 #t, 因此也就会运行
    (eternity x), 前面说了, eternity会永远运行, 所以 last-try 不会停机.
  2. 假设 last-try 不停机, 那么 (will-stop? last-try) 返回 #f, 因此也就不会运 行
    (eternity x), 那么 last-try 很显然就会返回. 所以 last-try 会停机

因此这就是个悖论.因此停机函数 will-stop?不存在

some example code(the little schemer)

racket

struct(新的数据类型) struct

语法形式: (struct struct-id (field-id ...))

  • struct-id : 是一个constructor, 可以用来构建一个该数据类型的实例
  • struct-id? :一个predication,测试是否是该数据类型的实例
  • struct-id-field-id: 从实例中获取 field-id 属性的值

下面来看个例子:

1
2
3
4
(struct posn (x y))                     ;定义一个posn类型
(define pos1 (posn 1 2)) ;struct-id: 构建一个posn实例
(posn? pos1) ;struct-id?: 是否为一个posn对象实例
(posn-x pos1) ;struct-id-field-id: 获得x属性

复制更新

根据已有对象更新其中的特定域然后返回新对象

语法形式: (struct-copy struct-id struct-expr [field-id expr] ...)

1
2
3
4
5
6
7
8
9
10
Examples:

> (define p1 (posn 1 2))
> (define p2 (struct-copy posn p1 [x 3]))
> (list (posn-x p2) (posn-y p2))

'(3 2)
> (list (posn-x p1) (posn-x p2))

'(1 3)

subtypes(类似于继承) subtypes

语法形式: (struct struct-id super-id (field-id ...))

1
2
(struct posn (x y))
(struct pos3d posn (z))

那么pos3d就有 (x y z) 三个属性

match(模式匹配) match

正则表达式只能用来匹配字符串,而 match 可以用来匹配任何的 scheme value, 它的语法形式如下:

1
2
(match target-expr
[pattern expr ...+] ...)

target-expr 与 pattern匹配, 如果匹配成功就执行后面的expr, 对pattern的 语法要做以下说明:

  • … 或 _ : 代表0次或者多次

  • ..k 或 _k : 代表至少 k 此 下面是一个将let转换为等价的lambda形式的例子

    1
    2
    3
    4
    5
    6
    7
    (match '(let ([a 1]
    [b 2])
    (set! a 11)
    (- a b))
    [`(let ([,var* ,val*] ...) ,body* ...)
    `((lambda (,@var*)
    ,@body*) ,@val*)])

    注意因为…的作用,上面的var*, val*, body*都是列表, 所以在后面需要使用 ,@来 分解.

  • literal: 字面值直接用 equal? 测试是否相等

    1
    2
    3
    4
    5
    6
    7
    (match 2
    [1 'one]
    [2 'two]
    [3 'three])
    (match #f
    [#t 'yes]
    [#f 'no])
  • (list lvp …): 会绑定对应的标识符,注意几个特殊符号,vector和list类似

    1
    2
    3
    4
    5
    6
    7
    8
    9
    (match '(1 + 2)
    [(list a '+ b) (+ a b)]) ;return 3

    (match '(1 2 3)
    [(list 1 a ...) a]) ;return '(2 3)

    (match '(1 2 3 4)
    [(list 1 a ..3) a]
    [_ 'else]) ;return '(2 3 4)

    上例中 a b 就被绑定为 1, 2

  • (struct-id pat …)或者(struct struct-id (pat …)):匹配一个实例,并且绑定 一些变量

    1
    2
    3
    (define-struct tree (val left right))
    (match (make-tree 0 (make-tree 1 #f #f) #f)
    [(tree a (tree b _ _) _) (list a b)]) ;'(0 1)
  • (struct struct-id _) :匹配任何 struct-id的实例

  • (? expr pat …): expr是一个predication, 只有它返回true的时候,才会匹配后 面的pat

    1
    2
    (match '(1 3 5)
    [(list (? odd?) ...) 'yes])

    它的工作原理是这样,以下面的例子为例:

    1
    2
    (match '((1 2) 3 6)
    [`(,(? pred? `(,a1 ,a2)) ,b ,c) `(,a1 ,b ,c)])

    如果上面的例子匹配, 那么b, c应该分别绑定3, 6, 那么前面的 (? pred? `(,a1 ,a2)) 应该匹配(1 2), 所以先将(1 2) 传递给pred?, 如果pred? 返回#t, 那么接 着用(1 2)
    去匹配后面的`(,a1 ,a2). 也就是说pred? 只会检查(? pred? …) 匹配 的那一部分.

  • (quasiquote qp): unqote或unquote-splicing的部分会绑定为变量,其它部分会原样匹配

    1
    2
    (match '(1 + 2)
    [`(,a + ,b) (+ a b)])

    a b都是unquote指定的部分,所以绑定为变量, 其它部分比如 + 就原样匹配, 可以和 (? expr pat …)结合使用:

    1
    2
    3
    (match '(+ a "hello")
    [`(+ a ,(? number? x)) x]
    [`(+ a ,(? string? x)) x])

    关于quasiquote的一些说明:

    • quasiquote(`): 和quote(‘)类似, 只是表达式中的unquote会求值在返回,如果没
      有unquote,那么它的行为就和 quote 一样,来看几个例子: (cons a b) 等价于
      `(,a ,b)

    • unquote(,): 指定的部分会先求职,在插入list中

      1
      2
      3
      (quasiquote (1 2 (unquote (+ 1 2)) (unquote (- 5 1))))
      ;;; equivalent
      `(1 2 ,(+ 1 2) ,(+ 5 1)) ;'(1 2 3 6)
    • unquote-splicing(,@): 和unquote的行为类似, 只是它所指定的表达式求值后必
      须返回list,这个list中的元素会拆开然后插入原list中

      1
      `(1 2 ,@(list (+ 1 2) (2 2)) 5)         ;'(1 2 3 4 5)

module

module的基本语法是:

1
2
(module name-id initial-module-path
decl ...)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
(module cake racket
(provide print-cake)

(define (print-cake n)
(show " ~a " n #\.)
(show " .-~a-. " n #\|)
(show " | ~a | " n #\space)
(show "---~a---" n #\-))

(define (show fmt n ch)
(printf fmt (make-string n ch))
(newline)))
  • name-id: module的名字,上例是 cake
  • initial-module-path: 初始化要导入的module, 上例是 racket
  • provide : 可选的,也就是规定哪些东西是可以导出的,上例中 print-cake 会导 出,但是 show 是模块私有,
    如果不提供 provide 那么所有的属性都是私有的, 所以一个模块必须指定 provide 才对导入者有意义

声明一个模块并不会直接对模块的 body 部分求值,只有当使用 require 明确导 入该模块时才会求值

submodule

一个文件只能包含一个顶层模块,这个模块可以通过 module 指定,也可以通 过 lang lang-name 来间接的指定,
那么在这个顶层模块中又可以定义子模块,比 如如下代码:

1
2
3
4
5
6
7
8
#lang racket
(module zoo racket
(provide tiger)
(define tiger "Tony"))

(require 'zoo)

tiger

上面的代码(假设文件名是park.rtk) #lang racket 指定了顶层模块, 而 zoo 是 一个子模块,
在顶层模块中可以直接通过 (require 'zoo) 来包含子模块, 如果是 在该文件以外,
你需要导入该文件的模块, 使用 (require "park.rtk") 只会导入 顶层模块, 使用submode语法比如
(require (submod "park.rtk" zoo)) 就可以导 入子模块了

  1. module* :module*:

    1
    2
    (module* name-id initial-module-path-or-#f
    decl ...)

    使用 module 声明的子模块可以可以被父模块导入, 但是子模块不能导入父模块, 而恰恰相反,使用 module*
    声明的模块可以导入父模块, 可是父模块不能导入该子 模块, 如果指定 #f 作为默认导入模块,
    那么父模块中的所有绑定在子模块中都可 见.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #lang racket

    (define (print-cake n)
    (show " ~a " n #\.)
    (show " .-~a-. " n #\|)
    (show " | ~a | " n #\space)
    (show "---~a---" n #\-))

    (define (show fmt n ch)
    (printf fmt (make-string n ch))
    (newline))

    (module* main #f
    (print-cake 10))

    在上面的 main 这个submodule中, 顶层模块的所有绑定都可见, 注意当一个
    submodule的名字为main时,有一个特殊的地方,也就是说当前的文件作为执行文件时
    (racket file-name.rtk),即便你没有使用 (require 'main) 语句, 这个子模块仍
    然会运行,和python的=ifmain= 很类似,所以这个main模块可以写一些本模块的测试 代码

  2. module+ :module+:

    1
    2
    (module+ name-id
    decl ...)

    等价于

    1
    2
    (module* name-id #f
    decl ...)

    module+一般用来写 test 模块, 多个test模块会合并为一个test模块, 使用raco test
    filename.rkt来运行测试代码

lang

racket的源文件一般需要使用 #lang lang-name 这样的方式来指定语言,这个实际是一 个module的简写方式,比如
#lang racket 等价于:

1
2
(module name racket
decl ...)

所以 #lang racket 的意思就是定义一个module, 该模块的名字一般继承自文件名, 然后将racket作为初始模块导入.

require

  1. 如果是文件那么应该使用这样的语法:

    1
    2
    3
    (require "aa.rkt")
    (require "../aa.rkt")
    (require "../subdirectory/aa.rkt")

    以当前文件的路径为当前路径,使用和shell类似的路径表达方式来确定需要导入的模 块的名字,记住要带扩展名

  2. 当前文件的子模块: (require 'name)

  3. 标准模块: (require racket)

  4. 子模块: (require (submod "aa.rkt" submod-name))

provide

  • 导出所有: (provide (all-defined-out))
  • 只导出通过require引入的绑定: (provide (all-from-out))
  • 导出除指定的外所有的绑定: (provide (except-out name ...))
  • 重命名: (provide (rename-out [orgn-id export-id]))

some example:

1
2
(provide run run-all)
(provide (all-defined-out))

typed racket

sample program

1
2
3
4
5
6
7
#lang typed/racket
(struct: pt ([x : Real] [y : Real]))

(: distance (pt pt -> Real))
(define (distance p1 p2)
(sqrt (+ (sqr (- (pt-x p2) (pt-x p1)))
(sqr (- (pt-y p2) (pt-y p1))))))

Types

Basic Types

  1. Number
  2. Char
  3. String
  4. Boolean

Function Type

(Number -> Number) (String String -> Number)

Union Type

当一个类型有几种变种时, 应该使用Union Type

1
2
3
4
5
6
7
8
9
10
11
#lang typed/racket
(define-type Tree (U leaf node))
(struct: leaf ([val : Number]))
(struct: node ([left : Tree] [right : Tree]))

(: tree-height (Tree -> Integer))
(define (tree-height t)
(cond [(leaf? t) 1]
[else (max (+ 1 (tree-height (node-left t)))
(+ 1 (tree-height (node-right t))))]))

上面的Tree就包含两种类型, Node与Leaf

Recursive Type

1
(define-type BinaryTree (Rec BT (U Number (Pair BT BT))))

Subtyping

任何类型都是Any的子类型.

Polymorphism

1
2
3
4
5
(: list-length (All (A) ((Listof A) -> Integer)))
(define (list-length l)
(if (null? l)
0
(add1 (list-length (cdr l)))))

注意上面的 All.

Footnotes


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!