Created
March 9, 2012 07:40
-
-
Save tiye/2005524 to your computer and use it in GitHub Desktop.
Scheme 学习笔记
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
在线版本: | |
http://docview.cnodejs.net/leaf/notes/scheme/scheme.lx?lx | |
打算学`Scheme`, 搜了不少尝试去理解, 中文资源不如`JS`多 | |
`IBM`社区`5+`篇文章, 下面两篇介绍语法方面比较清晰 | |
http://www.ibm.com/developerworks/cn/linux/l-schm/index1.html | |
http://www.ibm.com/developerworks/cn/linux/l-schm/index2.html | |
关于历史掌故可以看下面这篇了解下, 比较乱, 我没有细看 | |
http://blog.chinaunix.net/space.php?uid=20106293&do=blog&id=142113 | |
`scm`的规范简介有力, 真的很短, 入门后去看下 | |
直接`Google`就能找到 "算法语言`Scheme`修订`5`报告" | |
教程英文的不少, 中文有本`SICP`的翻译, 算清晰, 爱问搜索有 | |
我搜到`3`份英文教程, 打算只看最简短的第一份了 | |
`Teach Yourself Scheme in Fixnum Days` | |
http://www.ccs.neu.edu/home/dorai/t-y-scheme/t-y-scheme-Z-H-1.html | |
`How to Design Programs: DrScheme Companion` http://www.htdp.org/ | |
`The Scheme Programming Language` http://www.scheme.com/tspl3/ | |
我参照的这份文档只为学会用 scm 解决问题, 大不算深入 | |
然后我很想用上`Scheme`的缩进语法, 希望入门后去看 | |
http://srfi.schemers.org/srfi-49/srfi-49.html | |
记得例子不少用`guile`来运行`scm`的, 在脚本开头加两行并可以空行 | |
#! /usr/bin/env guile | |
!# | |
系统没有 guile 可以在 Ubuntu 安装, 我装的是 1.8 版本 | |
然后脚本我不重复了, 开头缩进是笔记格式, 代码参考原文 | |
;The first program | |
(begin | |
(display "Hello") | |
(newline)) | |
分号开头进行注释, `begin`表示后边多个模式 | |
`display`是向`console`输出, `newline`是输出新的换行 | |
教程说的`mzscheme`不清楚, `Ubuntu`里面用的`guile` | |
实际上我用的命令是`$ rlwrap guile` | |
然后输入`load "hi.scm"`(点钱目录文件名)运行该脚本 | |
`guile`中的`prompt`是`guile>`, 这里直接输入代码 | |
在`prompt`中输入`"hi"`会直接输出内容 | |
两种方式有区别, 向`console`输出对于函数是种副作用 | |
而`"hi"`则是计算得到结果的 | |
文章约定`=>`表示模式运算后给出结果 | |
可以用`(exit)`退出`guile`命令行, `Linux`常快捷键`C^d` | |
运行脚本可以用`guile -s hi.scm` | |
@`scm`有布尔, 数值, 字符, 符号几个数据类型 | |
真: `#t`, 假: `#f`, 判断是否布尔类型: `boolean? #t` | |
否定: `(not #t) ;=> #f` | |
`scm`中数值类型有整数, 分数, 实数, 复数 | |
各自有`number? complex? real? rational? integer?` 判断 | |
整数未必十进制, 前缀`#b #o #x`分别表示二, 八, 十六进制 | |
比如`#b100`是二进制的`100`, 十进制的`4` | |
判断大小是否相等用`(eqv? 2 #b10) => #t``(eqv? 2 2.0) => #f` | |
这个广义的判别函数对于不同类型不会报错`(eqv? 2 #f) => #f` | |
另外有个针对数值的判别符`(= 42 42.0) => #t` | |
而这个判别符对于数值外内容会报错, 比如`(= 2 #f)` | |
对于数字的大小判断还有`> < >= <=`可用 | |
运算符号有`+ - * /`, 都支持一个或多个参数 | |
其中除法结果是分数 | |
`(expt 2 3)`表示乘方, 只有两个参数 | |
`min max abs exp atan sqrt`等都可以推测 | |
`scm`的字符以`#\`开头比如`#\c`是字符`c` | |
一般是在后面跟一个字符, 但也有用多个字母描述的比如 | |
`#\newline #\tab #\space`, 也有`#\ `表示空格 | |
字符的判别: `char? #\c ;=> #t`, 字符还有大小的判别 | |
`char<? char<=? char=? char>=? char>?` | |
为忽略大小写用`(char-ci=? #\A #\a) => #t`, 以此类推 | |
字母大小写转换用`char-downcase char-upcase` | |
前面这些比如`#t 42 #\c`自求值的内容 | |
符号被输入到解释器里, 给出运算结果一般就是本身 | |
符号类型不同, 因为同样的内容常被用作变量标识符 | |
意味着那会被计算, 并返回计算结果的内容 | |
但符号依然是基本的数据类型, 可以和其他类型交换 | |
在`scm`里用上`(quote xyz) => xyz`标记符号 | |
符号非常常用, 于是有了简写`'E`相当于`(quote E)` | |
符号的表示以不被混淆为准, `<=>`和'$!#*'都行 | |
但像这些`#t "aString" -i`就不行了 | |
可以用`(symbol? 'xyz)`判别 | |
注意`scm`里对大小写不敏感, 这里不例外 | |
接着可以定义符号为变量`(define xyz 9)` | |
在解释器里输入就会直接返回内容了`xyz ;=> 9` | |
或者用`(set! xyz #\c)`这样 | |
复合数据类型由数据类型俺结构组合而成 | |
字符串是自求值的`"Hello" => "Hello"` | |
该程序由一系列字符组成`(string #\H #\e #\l #\l #\o)` | |
用`(string-ref "abcd" 1)`取出序号`1` 的元素 | |
用`(string-append "a" "b" "c")`来组合新的字符串 | |
可新建指定长的空字符串`(make-string 3) ;=> "\x00\x00\x00")` | |
如果`(define s (make-string 3))` | |
那么再给`s`赋值就注意不能越界 | |
`(string-set! s 0 #\s)`用来修改指定序号的字符 | |
向量可以容纳各种类型, 包括向量自身 | |
定义向量: `(vector 1 2 3) ;=> #(1 2 3)` | |
也可以直接使用`#(1 2 3)`产生向量 | |
类似有`(make-vector 5)`来生成限定长度的向量 | |
类似有`vector-ref vector-set vector?` | |
@点对用来组合任意两个值, 前者称`car`, 后者`cdr` | |
组合两者的程序是`(cons 1 #\t) ;=> (1 . #\t)` | |
简洁的定义的方式`'(1 . #\t)` | |
(define x '(1 . #\t)) | |
取出内容通过`(car x)`或者`(cdr x)` | |
设置: `(set-car! x)`和`(set-cdr! x)` | |
(define y (cons (cons 1 2) 3)) ;=> ((1 . 1.0) . 2)) | |
(define y (cons 1 (cons 2 3))) ;=> (1 2 . 3) | |
`(cdr (car y))`可以简化成`(cdar y)`最多四层 | |
(cons 1 (cons 2 (cons 3 (cons 4 5)))) ;=> (1 2 3 4 . 5) | |
有个空的点对`'() ;=> ()` | |
'(1 . (2 . (3 . (4 . ())))) ;=> (1 2 3 4) | |
(cons 1 (cons 2 (cons 3 (cons 4 '())))) ;=> (1 2 3 4) | |
还有个程序: `(list 1 2 3 4) ;=> (1 2 3 4)` | |
还有: `'(1 2 3 4) ;=> (1 2 3 4)` | |
(define y (list 1 2 3 4)) | |
(list-ref y 0) ;=> 1 | |
(list-ref y 3) ;=> 3 | |
(list-tail y 1) ;=> (2 3 4) | |
(list-tail y 3) ;=> (4) | |
(pair? '(1 . 2)) ;=> #t | |
(pair? '(1 2)) ;=> #t | |
(pair? '()) ;=> #f | |
(list? '()) ;=> #t | |
(null? '()) ;=> #t | |
(list? '(1 2)) ;=> #t | |
(list? '(1 . 2)) ;=> #f | |
字符串和数值间通过`ASIIC`码互转, 其他较明显 | |
(char->integer #\d) ;=> 100 | |
(integer->char 50) ;=> #\2 | |
(string->list "hello") ;=> (#\h #\e #\l #\l #\o) | |
(number->string 16) ;=> "16" | |
(string->number "16") ;=> 16 | |
(string->number "hi") ;=> #f | |
(symbol->string 'symbol) ;=> "symbol" | |
(string->symbol "string") ;=> string | |
基于基数的转化, 比如下面基于八进制 | |
`(string->number "16" 8)` | |
`scm`还有个过程类型`procedure`, 目前看到都是基本过程 | |
基本过程在环境被支持, 也有途径创建自己的过程 | |
另有种数据类型`port`端口, 关联文件和终端的输入输出 | |
比如`display`还有个隐含的参数, 表示输出的端口 | |
`(display "Hello, World!" (current-output-port)` | |
`scm`解释器会探测每一个形式`(form)`首个符号 | |
如果那是过程, 就会将其余作为参数执行这个形式 | |
像`begin define set!`是一些特殊形式, 特殊行为 | |
用户可以通过`lambda`表达式创建过程 | |
(lambda (x) (+ x 2)) | |
((lambda (x) (+ x 2)) 5) ;=> 7 | |
(define add2 (lambda (x) (+ x 2))) | |
(add2 4) ;=> 6 | |
(define area | |
(lambda (length breadth) | |
(* length breadth))) | |
(define area *) ;=> 同上 | |
`apply`可以运行一个过程, 并载入指定参数 | |
可以载入多个参数, 但必须要列表作为参数结尾 | |
(define x '(1 2 3)) | |
(apply + x) ;=> 2 | |
(apply + 1 2 3 x) ;=> 12 | |
`begin`用来俺顺序执行参数中的子形式 | |
而`lambda`内部也是顺序执行的: | |
(define display3 | |
(lambda (arg1 arg2) | |
(display arg1) | |
(newline) | |
(display arg2)) | |
条件语句`if`, `else`是隐含的 | |
(if #t | |
(display "true")) | |
(if (> 1 0) | |
(display "true") | |
(display "false")) | |
`when`用来判断, 当为真, 按顺序执行子形式 | |
`unelss`把`when`的条件取反, 然后顺序执行 | |
不过两者在`guile`里用不出来, 不考虑了 | |
`cond`用于判别, 每个参数都有判别是和结果 | |
有可选的`else`, 相似有`case`, 看例子的括号 | |
(define c #\c) | |
(cond ((char<? c #\c) -1) | |
((char=? c #\c) 0) | |
(else 1)) ;=> 0 | |
(case c | |
((#\a) 1) | |
((#\c) 2) | |
(else 3)) | |
`and or not`对应: 且, 或, 非 | |
不过在`guile`对于多个值或其他类型参数就要出错 | |
(and #\t #\f) ;=> #\f | |
(or #\t #\f) ;=> #\t | |
@`scm`的变量作用域是词法域, 静态作用域 | |
全局变量不受局部变量的影响, 看例子 | |
(define x 9) | |
(define add 2 (lambda (x) (+ x 2))) | |
x ;=> 9 | |
(add2 3) ;=> 5 | |
x ;=> 9 | |
而`(set! x 20)`能对全局变量进行修改 | |
`scm`会选取词法上(?)最近的变量进行使用 | |
(define counter 0) | |
(define bump-counter | |
(lambda () | |
(set! counter (+ counter 1)) | |
counter)) | |
(bump-counter) ;=> 1 | |
(bump-counter) ;=> 2 | |
`let`可以新建局部变量, 遮盖全局变量, 注意写法 | |
(define x 20) | |
(let ( | |
(x 1) | |
(y 2)) | |
(list x y)) ;=> (1 2) | |
(let ( | |
(x 1) | |
(y x)) | |
(list x y)) ;=> (1 20) | |
考虑到`let*`还有下面的写法 | |
(let* ((x 1) | |
(y x)) | |
(+ x y)) ;=> 2 | |
(let ((x 1)) | |
(let ((x y)) | |
(+ x y))) ;=> 2 | |
(let ((cons (lambda (x y) (+ x y)))) | |
(cons 1 2)) ;=> 3 | |
`fluid-let`会临时修改全局变量值, 过后复原 | |
但是这个在`guile`里头也是不对劲的(?) | |
而`let`本身面对这样的过程处理不同 | |
(define x 0) | |
(define x+ | |
(lambda () | |
(set! x (+ x 1)) | |
(display x))) | |
(let ((x 20)) | |
(x+)) ;=> 1 | |
@递归, 调用自身, 转化条件, 设定边界, 看例子 | |
(define fractorial | |
lambda (n) | |
(if (= n) 1 | |
(* n (fractorial (- n 1))))) | |
(define is-even? | |
(lambda (n) | |
(if (= n 0) #t | |
(is-odd? (- n 1))))) | |
(define is-odd? | |
(lambda (n) | |
(if (= n 0) #f | |
(is-even? (- n 1))))) | |
其实`scm`已经内置`even? odd?`两个过程 | |
注意在局部变量实现`is-even? is-odd?`会有问题 | |
用`let`时`if`内部的`is-even? is-odd?`不能正确指向 | |
用`let*`时`if`内部的`is-odd`不能正确指向 | |
于是引入了新的过程`letrec`进行 | |
(letrec ((local-even? (lambda (n) | |
(if (= n 0) #t | |
(local-odd? (- n 1))))) | |
(local-odd? (lambda (n) | |
(if (= n 0) #f | |
(local-even? (- n 1)))))) | |
(list (local-even? 23) (local-odd? 23))) | |
`letrec`是专门为定义递归和相互递归的局部过程设计的 | |
借助`letrec`实现的循环过程 | |
(letrec ((countdown (lambda (i) | |
(if (= i 0) 'liftoff | |
(begin | |
(display i) | |
(newline) | |
(countdown (- i 1))))))) | |
(countdown 10)) | |
这一段可以用宏用`let`写更紧凑的结构 | |
(let countdown ((i 10)) | |
(if (= i 0) 'liftoff | |
(begin | |
(display i) | |
(newline) | |
(countdown (- i 1))))) | |
`scm`中不存在递归以外其他循环和迭代的构造 | |
递归会被关照以免开销过大(?) | |
前面的递归是尾部递归, 尾递归可以被优化, 因而安全 | |
例子, 尾递归实现, 从列表`l`取元素`o`位置, 否则返回`#f` | |
例子, 递归倒转列表内容的顺序 | |
(define reverse! | |
(lambda (s) | |
(let loop ((s s) (r '())) | |
(if (null? s) r | |
(let ((d (cdr s))) | |
(set-cdr! s r) | |
(display d) | |
(display ", ") | |
(display s) | |
(newline) | |
(loop d s)))))) | |
几个小时才看明白, 还好环境里一般会提供这个函数的 | |
有一类迭代多次重复, 就是遍历列表每个元素 | |
于是有`map for-each`两种操作, 前者熟悉的 | |
(define add2 (lambda (x) | |
(+ x 2))) | |
(map add2 '(1 2 3)) ;=> (3 4 5) | |
(map cons '(1 2 3) '(10 20 30)) ;=> ((1 .10) (2 . 20) (3 30)) | |
(map + '(1 2 3) '(10 20 30)) ;=> ( 11 22 33) | |
而后者没有返回值, 是副作用 | |
(for-each display '(1 2 3 4)) | |
`scm`的读取端口的参数是可选的, 默认是`console` | |
读取内容以字符, 行, `S 表达式`为单位 | |
以某种`EOF`结束, 才能用`eof-object?`判断结束 | |
函数有`read-char read-line read`, 后者读取`S 表达式` | |
写入的端口也是可选的, 默认`console` | |
写入的单位可以是字符, 或者基于`S 表达式` | |
`write-char`写出的时候不会有`#\`前置, 保留机器方便的格式 | |
`display`的功能类似, 但会更方便人们阅读 | |
`open-input-file`过程接收文件名, 返回输出端口 | |
(define i (open-input-file "hello.txt")) | |
(read-char i) ;=> #\h | |
(define j (read i)) | |
j ;=> ello | |
如果原文件不存在, `close-output-file`会建立新文件 | |
运行代码发现`output input file port`易混淆, 注意 | |
(define o (open-output-file "g.txt")) | |
(display "hello" o) | |
(write-char #\space o) | |
(display 'world o) | |
(newline o) | |
(close0output-port o) | |
`call-with-input-file`和`call-with-output-file`自动管理关闭 | |
(call-with-input-file "hello.txt" | |
(lambda (i) | |
(let* ((a (read-char i)) | |
(b (read-char i)) | |
(c (read-char i))) | |
(list a b c)))) ;=> (#\h #\e #\l) | |
未来操作字符串方便有`open-input-string open-output-string` | |
(define i (open-input-string "hello world")) | |
(read-char i) ;=> #\h | |
(read i) ;=> ello | |
(read i) ;=> world | |
(define o (open-output-string)) | |
(write 'hello o) | |
(write-char #\, o) | |
(display " " o) | |
(display "world" o) | |
(get-output-string o) ;=> "hello, world" | |
还有`load load-ralative`两个形式, 解说比较复杂(?) | |
宏按说明应该像重用的代码块的缩写之类, 像链接用 | |
(defint-macro when | |
(lambda (test . branch) | |
(list 'if test | |
(cons 'begin branch))))) | |
用`define-macro`定义, 结果是 | |
(when (< 1 2) | |
(display "true 1\n") | |
(display "true 2\n")) | |
相当于代入其中转化成为 | |
(if (< 1 2) | |
(begin | |
(display "true 1\n") | |
(display "true 2\n"))) | |
同样有`unless`的使用, 甚至在其中使用已经定义的`when` | |
(define-macro unless | |
(lambda (test . branch) | |
(list 'if | |
(list 'not test) | |
(cons 'begin branch)))) | |
(define-macro unless | |
(lambda (test . branch) | |
(cons 'when | |
(cons (list 'not test) branch)))) | |
用反引号还可以简化语法, 大概`guile`对大小写敏感 | |
其中`,`表示插入相应的值, `,@`表示插入相应`S 表达式` | |
(defin-macro when | |
(lambda (test . branch) | |
`(if ,test | |
(begin ,@branch)))) | |
当用下面这种方式实现一个`or`形式 | |
(define-macro my-or | |
(lambda (x y) | |
`(if ,x ,x ,y))) | |
(my-or 1 2) ;=> 1 | |
(my-or #f 2) ;=> 2 | |
(my-or | |
(begin | |
(display "twice") | |
(newline) | |
#t) | |
2) | |
上面因为求值过程, 会出现两次`display`, 改写 | |
(define-macro my-or | |
(lambda (x y) | |
`(let ((temp ,x)) | |
(if temp temp ,y)))) | |
再改写用来形成私有的调用, 甚至用`gensym`产生私有的符号 | |
(define-macro my-or | |
(lambda (x y) | |
`(let ((+temp ,x)) | |
(if +temp +temp ,y)))) | |
(define-macro my-or | |
(lambda (x y) | |
(lat ((temp (gensym))) | |
`(let ((,temp ,x)) | |
(if ,temp ,temp ,y))))) | |
`? 8.3 的 fluid-let 在 guile 没有, 正好跳过` | |
后边`Mzscheme`和`guile`差别较大, 加上难度, 放弃了 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment