leafsummer
3/11/2016 - 3:15 AM

Scheme 学习笔记

Scheme 学习笔记

在线版本:
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`差别较大, 加上难度, 放弃了