qinguan's Blog

Happy coding

输入输出

——chapter7 I/o

Teach Yourself Scheme in Fixnum Days


读取单个字符 :read-char

> (read-char)
f
#\f
> (read-char)
#\newline
>


如上所示,输入(read-char)回车后,DrRacket会出来输入框,我输入f回车。
首先解释器会打出#\f,我再输入(read-char),则会输出回车。read-char每次只读取一个字符,若用户第一次输入多个字符,
则接着输入(read-char)时,自动会读取下面的字符,并进行输出。

读取一行字符:read-line

> (read-line)
kkkkkkkk
"kkkkkkkk"
>


read-line返回的字符串并不包含换行符。
读取还有另一个过程read,不分字符还是字符串:

> (read)
dekdekdl
'dekdekdl
>


写单个字符:write-char

> (write-char #\d)
d
>


写字符串:wirte

> (write "hello")
"hello"
>


写过程默认输出在控制台。默认输入输出端口为current-input-port和current-output-port。

(display 9)
(display 9 (current-output-port))


上面两行语句结果相同。

文件输入输出

(define i (open-input-file "hello.txt"))
定义输入文件,可用read-过程读取文件信息
(read-char i)
读取单个字符
(read-line i)
读取一行信息
(define o (open-output-file "hello1.txt"))
定义写入文件,可以write-、display过程将信息写入文件
(display "hello" o)
将hello写入文件"hello1.txt"
(write-char #\space o)
将空格写入文件
(display 'qin o)
将字符qin写入文件
(newline o)
将换行符写入文件
(close-output-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))))


上面代码输出hello.txt的前三个字符。      

call-with-output-file在R5RS中
(call-with-output-file "hello1.txt"
  (lambda (p)
    (let f ((ls (list #\a #\b #\c)))
      (if (not (null? ls))
          (begin
            (write (car ls) p)
            (newline p)
            (f (cdr ls)))))))


上面代码将(list #\a #\b #\c))写入文件hello1.txt

字符串的读取与写入

open-input-string 在#lang racket中
(define i (open-input-string "hello world"))
(read-char i)
(read i)
(read i)
输出:
#\h
'ello
'world


read以空格为分界符,输出字符串的每一个单词。
 

(define o (open-output-string))
定义一个字符串输出对象 o
(write 'hello o)
(write-char #\, o)
(display " " o)
(display "world" o)


读取字符串对象o的内容,使用get-output-string过程
(get-output-string o)

文件加载load & load-relative
chapter7.rkt文件为前面字符串输出对象及相关操作

> (load "chapter7.rkt")
> (get-output-string o)
"hello, world"
> (load-relative "chapter7.rkt")
> (get-output-string o)
"hello, world"
>


load-relative在MzScheme及racket中均有。

递归

——chapter6 Recursion

Teach Yourself Scheme in Fixnum Days


递归是个好东西,可以很清晰的刻画出程序流程来。
下面的递归程序是求斐波纳契亚数列。

(define factorial
  (lambda (n)
    (if (= n 0) 1
        (* n (factorial (- n 1))))))

而接下来的例子显示了递归互引用。
判断数字是否为偶数的is-even引用了判断是否为奇数的is-odd,两者交互引用。

(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)))))      

 
is-eve?和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? 3) (local-odd? 3)))
=>  (#f #t)


等价语句块:

(begin
  (define local-even? (lambda (n)
                        (if (= n 0) #t
                            (local-odd? (- n 1)))))
  (define local-odd? (lambda (n)
                       (if (= n 0) #f
                           (local-even? (- n 1)))))
  (list (local-even? 3) (local-odd? 3)))  
=>  (#f #t)


用递归来表示循环
在scheme中,没有循环语句,只能通过递归俩表示循环。下面的代码循环输出10-1的数字:

(letrec ((countdown (lambda (i)
                      (if (= i 0) "done"
                          (begin
                            (display i)
                            (newline)
                            (countdown (- i 1)))))))
  (countdown 10))
 
(let countdown ((i 10))
  (if (= i 0) "done"
      (begin
        (display i)
        (newline)
        (countdown (- i 1)))))


上面两段代码结果相同:

10
9
8
7
6
5
4
3
2
1
"done"        


如上面两段代码所示,Scheme在实现循环的过程中使用了尾递归。
尾递归的另外一个例子:

(define list-position
  (lambda (o l)
    (let loop ((i 0) (l l ))
      (if (null? l) #f
          (if (eqv? (car l) o) i
              (loop (+ i 1) (cdr l)))))))
(define test-list (list #\a #\b #\c #\d))
(list-position #\c test-list)
=>  2


若o在list中存在,则该段代码返回list中的对象o的位置,否则返回#f。

(define reverse!
  (lambda (s)
    (let loop ((s s) (r '()))
      (if (null? s) r
          (let ((d (cdr s)))
            (set-cdr! s r)
            (loop d s))))))
(reverse! (list #\a #\b #\c #\d))
=>  (#\d #\c #\b #\a)


该段代码返回list列表逆序输出。但该段代码运行后,会改变list的内容。list最后返回值为逆序输出的最后一个值,上例中即为#\a。set-cdr!在R5RS中。

将过程在列表中遍历
主要通过两种方式,map和for-each。

(map add2 '(1 2 3))
=> (3 4 5)


map将add2过程在列表上遍历,每个元素加2。

> (for-each display
            '("one" "two" "three"))
onetwothree


for-each将display过程在列表上遍历,打印每一个列表元素。
另外两个例子:

> (map cons '(1 2 3) '(10 20 30))
((1 . 10) (2 . 20) (3 . 30))
> (map + '(1 2 3) '(10 20 30))
(11 22 33)
>  (map + '(1 2 3) '(10 20 30) '(100 200 300))
(111 222 333)
> (map + '(1 2 3) '(10 20 30) '(100 200))
. . mcar: expects argument of type <mutable-pair>; given ()
>


map可以将过程加载在n个参数上,但每个参数形式应该一致。
map会在每一个参数列表上选取相应的元素,进行运算。

词法变量

 

——chapter5 Lexical variables

Teach Yourself Scheme in Fixnum Days

同名局部变量会覆盖全局变量
(define x 9)
(define add2 
  (lambda (x) 
    (set! x (+ x 2))
    x))
add2过程将返回参数值加2,在add2中将局部变量x置为x+2,全局变量并没有改变。
(define counter 0)
(define bump-counter
  (lambda ()
    (set! counter (+ counter 1))
    counter))
(add2 5) => 7
(bump-counter) =>1
(bump-counter) =>2
(bump-counter) =>3
counter => 3
bump-counter是无参过程,将全局变量递增,它修改了全局变量的值。
 
let & let*
 
(define x 9)
(let ((x 1)
      (y 2)
      (z 3))
  (list x y z))   => '(1 2 3)
let语句块定义了x,y,x三个局部变量。
(let ((x 2)  => x 定义为2
      (y x)) => y定义为x,该x为全局变量
  (list x y)) => '(2 9)
;验证可以通过将x改写成m,即:
  (let ((m 2)
      (y m))
  (list m y))
m未用全局变量定义,则系统会报m错误:expand: unbound identifier in module in: m
(let* ((x 2)
      (y x))
  (list x y)) => '(2 2)
采用let*则在语句块内,后面定义的局部变量可以采用该语句块内先前定义的变量来定义新的变量。
 
下面的例子是另一种情况,let中包含let,则子let语句块内新定义的变量可以通过外层语句块内的变量来赋值。
(let ((x 1))
  (let ((y x))  => 该x即为外层let的x值
    (+ x y)))  =>  2
let可以重写已定义的过程,如下例的cons:
(let ((cons (lambda (x y) (+ x y))))
  (cons 1 2))  => 3
在该语句块外cons依然定义的是点对:
(cons 1 2)  =>  '(1 . 2)  
 
使用fluid-let将临时值覆盖全局变量,则当前语句块中bump-counter使用的均为局部变量counter:
(fluid-let ((counter 99))
(display (bump-counter)) (newline)   =>  100
(display (bump-counter)) (newline)  =>  101
(display (bump-counter)) (newline))  =>  102
fluid-let在mzscheme中,故在DrRacket中使用fluid-let,需要在文件开头添加#lang mzscheme。
(let ((counter 99))
(display (bump-counter)) (newline)   =>  1
(display (bump-counter)) (newline)  =>  2
(display (bump-counter)) (newline))  =>  3

上面let语句块中bump-counter读取的依然是全局变量中的counter值,而不是该语句块中的counter值。

条件语句

 

——chapter4 Conditionals

Teach Yourself Scheme in Fixnum Days

 
条件语句形如:
(if test-expression
then-branch
else-branch)
基本上雷同所有编程语言。
> (define p 90)
> (if (> p 50)
      'safe
      'unsafe)
safe
> 
 
when & unless
当只有一个分支时,可以采用上面两种条件语句。
(define p 5)
(when (< p 4)
  (display p)
  (set! p (+ p 1))
  (newline))
(unless (< p 4)
  (display p)
  (set! p (+ p 1))
  (newline))
这里的when和unless并不是循环语句,它只是等价于只有一个分支的if语句。
 
cond
cond可以改写if嵌套语句。如以下的if语句:
(if (< p 0) -1
   (if (< p 5) 0
       1))
可以改写成等价的cond语句:
(cond 
  ((< p 0) -1)
  ((< p 5) 0)
  (else 1))
cond语句也隐式包含begin。
 
case
类似于cond语句。
(define c #\b)
(case c
  ((#\a) 1)
  ((#\b) 2)
  ((#\c) 3)
  (else 4))
 
and and or
Scheme还提供了布尔连接符and和or。
如and参数中含有#f,则返回#f,否则返回最后一个真值。
> (and 2 '() 3)
3
> (and 2 '() 3 #f 9)
#f
> 
若or参数中有真值,则返回第一个真值
> (or 1 2)
1
> (or #f 1)
1
> (or 3 #f)
3
> 

 

Forms

 

——chapter3 Forms
Teach Yourself Scheme in Fixnum Days
 
在Scheme中,程序即数据。
到目前为止我们接触到的所有样例程序均是s-expressions。
但并不是所有的s-expressions都是自估值的,比如说点对。
在解释器中输入点对,会得到错误信息:
> (1 .2)
. . procedure application: expected procedure, given: 1; arguments were: 0.2
> '(1 . 2)
(1 . 2)
 
过程
Scheme提供了一些元过程,比如说cons、string->list,我们也可以通过lambda来定义:
> ((lambda (x) (+ x 2)) 3)
5
> (define add 
    (lambda (x) (+ x 2) ))
> (add 3)
5
> (define area 
    (lambda (widh height) (* widh height)))
> (area 2 4)
8
> (define area *)
> (area 2 4)
8
> 
define area * ,则area相当于对参数连乘操作。
有些过程可以接受变参。lambda的参数可以是一个列表,形式通常为(x ...)或者(x ... .z)。
当参数是点对的时候,点之间的参数被绑定到lambda上,点之后的参数被绑定到一个列表上。
 
apply
> (define x '(1 3 2 ))
> (apply + x)
6
> (apply + 1 2 3 x)
12
> 
 
序列
在scheme语句,lambda默认包含begin语句,如下两段代码效果一样:
(define display3
  (lambda (arg1 arg2 arg3)
    (begin
      (display arg1)
      (display " ")
      (display arg2)
      (display " ")
      (display arg3)
      (newline))))
(display3 1 2 3)
(define display_3
  (lambda (arg1 arg2 arg3)
      (display arg1)
      (display " ")
      (display arg2)
      (display " ")
      (display arg3)
      (newline)))
(display_3 1 2 3)
 

2.2 复合数据类型 & 2.3 & 2.4

 

——chapter2 Data types

Teach Yourself Scheme in Fixnum Days

 
字符串类型
字符串用双引号括起来,也可以通过将字符参数传入string中:
> "hello world"
"hello world"
> (string #\h #\e #\l #\l #\o) 
"hello"
> 
字符串中的每一个字符可以分别读取。
> (define greeting "hello;hello!")
> (string-ref greeting 0)
#\h
> (string-ref greeting 3)
#\l
> 
字符串之间也可以连接:
> (string-append "e " "pluribus" "Unum" "!" )
"e pluribusUnum!"
可以先构造一个固定长度的字符串,然后填入相应的字符:
> (define a-3-char-long-string (make-string 3))
> (string? a-3-char-long-string)
#t
> a-3-char-long-string
"\u0000\u0000\u0000"
> (string-set! a-3-char-long-string 1 #\g)
> a-3-char-long-string
"\u0000g\u0000"
> 
使用string、make-string以及string-append构造的字符串都是可变字符串,均可以使用string-set!来改变字符串中相应的字符。
而直接用define定义的字符串就不行,如上面的greeting:
> (string-set! greeting 1 #\g)
string-set!: expects type <mutable string> as 1st argument, given: "hello;hello!"; other arguments were: 1 #\g
> 
 
向量
向量通过关键字vector定义,向量的元素可以为任何类型,包括向量,向量均带有前缀#:
> (vector 0 1 2 3 4)
'#(0 1 2 3 4)
> (vector 'q 'b 'c)
'#(q b c)
> (vector (vector 1 2 3) (vector "h" "k" "m"))
'#(#(1 2 3) #("h" "k" "m"))
> 
可以通过make-vector来构造向量,并设置长度:
> (define v (make-vector 5))
> (vector? v)
#t
> v
'#(0 0 0 0 0)
> (vector-ref v 0)
0
> (vector-set! v 2 8)
> v
'#(0 0 8 0 0)
> 
点对和列表
点对通过cons构造,点对的第一个元素叫car,第二个为cdr,可以通过car和cdr读取点对的相应元素,点对前面需要单引号:
> (cons 1 #t)
'(1 . #t)
> '(1 . #f)
'(1 . #f)
> (1 . #f)
. application: bad syntax in: (1 . #f)
> car
#<procedure:car>
> (car (cons 2 3))
2
> (define x (cons 4 #\g))
> (car  x)
4
> (cdr x)
#\g
> 
点对也有car-set!和cdr-set!,但在DrRacket中,貌似有点问题,不支持:
> (set-car! x 2)
reference to an identifier before its definition: set-car!
> (set-cdr! x #\j)
reference to an identifier before its definition: set-cdr!
 
在scheme48 1.8版本的实现中,支持car-set!和cdr-set!。
 
点对也能包含点对本身,同时点对的读取可以通过c...r-style,如下所示:
> (define y (cons (cons 1 2) 3))
> y
'((1 . 2) . 3)
> (car (car y))
1
> (caar y)
1
> (cdar y)
2
> (define z (cons (cons (cons 1 2) 3) 4))
> (caaar z)
1
> (cdaar z)
2
> 
当点对都整齐的出现在点对的第二个元素上时,Scheme采用了一种简单的缩写方式:
> (cons 1 (cons 2 (cons 3 (cons 4 5))))
'(1 2 3 4 . 5)
> 
若最后一个点对的第二个元素为空,则()前须带单引号:
> (cons 1 (cons 2 (cons 3 (cons 4 ()))))
. #%app: missing procedure expression; probably originally (), which is an illegal empty application in: (#%app)
> (cons 1 (cons 2 (cons 3 (cons 4 '()))))
'(1 2 3 4)
> 
可以通过pair来判断是否是点对:
> (pair? '(1 . 3))
#t
> 
Scheme还提供了列表list,python中也有,这是一个好东西:
> (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)
4
> (list-tail y 1)
'(2 3 4)
> (list-tail y 3)
'(4)
> (list? y)
#t
> 
list-ref返回指定索引值,而list-tail则返回包含该索引值之后的所有列表元素,可以通过list?来判断是否是列表。
'()由null?来判断。
> (null? '())
#t
> 
数据类型转换
Scheme中数据类型的转换相当方便:
> (char->integer #\d)
100
> (integer->char 50)
#\2
> (string->list "hello")
'(#\h #\e #\l #\l #\o)
> 
此外还有list-string、vector-list、list-vector。
数字也可以转换成字符,字符也可以转换为相应的数字,如果该数字存在的话:
> (number->string 16)
"16"
> (string->number "16")
16
> (string->number "Am I a hot number?")
#f
> 
string->number还可以带一个可选的参数,表示进制:
> (string->number "16" 8)
14
> 
字符串和符号间也可以相互转换:
> (symbol->string 'symbol)
"symbol"
> (string->symbol "string")
'string
> 
 
其它数据类型
Scheme还提供了其它的数据类型,比如说例程。
之前看到的display、cons、+等等都是。
另一个数据类型是端口port,类似于管道,用于输入输出。
display可以带两个参数,第一个是输出的内容,第二个是输出的地方,文件或者终端。
> (display "Hello, World!" (current-output-port))
Hello, World!
> 
这里的current-output-port表示当前终端。
 
上面讨论的所有数据类型均是符号表达式。

 

2.1 简单数据类型

——chapter2 Data types

Teach Yourself Scheme in Fixnum Days

布尔类型
Scheme的布尔类型用#t表示true,用#f表示false
> (boolean? #t)
#t
> (boolean? "fd")
#f
> (not #f)
#t
> (not #t)
#f
> (not 't) 
#f
> 
从上面也可以看出,Scheme将所有不是false的值均作为真值。
 
数字类型
Scheme的数字类型有整型、有理数、实数及复数。
> (number? 42)
#t
> (number? #t)
#f
> (complex? 2+3i)
#t
> (complex? 2)
#t
> (real? 3.21)
#t
> (real? 22/7)
#t
> (rational? 2+3i)
#f
> (rational? 22/7)
#t
> (integer? 22/7)
#f
> (integer? 23)
#t
> 
整型进制表示
> #b1100 =>2进制
12
> #o77  =>8进制
63
> #d12  =>10进制
12
> #x12  =>16进制
18
> 
可以用eqv?来判断两个参数是否相等
> (eqv? 42 42)
#t
> (eqv? 42 #f)
#f
> (eqv? 42 42.0)
#f
> (eqv? #b1100 #d12)
#t
> (eqv? #b1100 #x12)
#f
> (eqv? #b1100 #xd)
#f
> (eqv? #b1100 #xc)
#t
> 
如果比较的两个数均为数字,也可以用=
> (= 42 42)
#t
> (= 42 #f)
=: expects type <number> as 2nd argument, given: #f; other arguments were: 42
> 
此外,eqv?是严格相等的,包括数据类型,同样是42,如果一个是浮点数,一个是整型,eqv?报false,而=却显示true:
> (= 42 42.0)
#t
> (eqv? 42 42.0)
#f
数字之间的比较还有<, <=, >, >=:
> (< 4 't)
<: expects type <real number> as 2nd argument, given: 't; other arguments were: 4
> (< 4 5.6)
#t
> 
对于单个数字参数,-返回参数的相反数,而/返回参数的倒数:
> (- 4)
-4
> (/ 4)
1/4
> (/ 0)
/: division by zero
> (- 0)
0
> 
数值中还有max和min过程,要求输入参数类型为实数,max返回输入参数中的最大值,min返回输入数值中的最小值:
> (max 1 2 3 4)
4
> (min 1 2 3 4)
1
> (max #t 2 3 4)
max: expects type <real number> as 1st argument, given: #t; other arguments were: 2 3 4
> 
abs过程返回参数的绝对值:
> (abs -3.2)
3.2
> 
 
字符类型
Scheme中所有字符类型数据均有前缀#\表示,比如说#\c表示字符c,特殊字符换行用#\newline,tab用#\tab,空格用#\ 或#\space。
字符类型断言用char?:
> (char? #\c)
#t
> (char? 1)
#f
> (char? #\;)
#t
> 
字符类型比较用char<?,char=?,char>?:
> (char<? #\a #\b)
#t
> (char>? #\a #\b)
#f
> (char=? #\a #\a)
#t
> (char=? #\a #\A)
#f
> (char>? #\a #\A)
#t
> 
 
若大小写不敏感,则用char-ci=?:
> (char-ci=? #\a #\A)
#t
> 
将大写字符转化为小写用char-downcase,相反用char-upcase:
> (char-downcase #\A)
#\a
> (char-upcase #\a)
#\A
> 
 
符号类型
该类型用单引号做前缀或标注quote:
> (eqv? (quote E) 'E)
#t
> (symbol? 'xyz)
#t
> (symbol? 42)
#f
> 
本书中Scheme 符号类型是大小写不敏感:
Scheme symbols are normally case-insensitive. Thus the symbols Calorie and
calorie are identical:
(eqv? ’Calorie ’calorie)
? #t
但在DrRacket5.1.1中大小写敏感:
> (eqv? (quote E) 'e)
#f
> (eqv? 'E 'e)
#f
可见不同的Scheme实现有差别。
我们可以用define来定义全局变量:
(define size 2)
> size
2 
>
则size的值就为2。
同时可以用set!来改变这个值:
> (set! size "3")
> size
"3"
> 
 

Hello world

chapter1 Enter scheme

——Teach Yourself Scheme in Fixnum Days 

一直诱惑于函数式编程语言,曾经鼓捣过一阵Haskell,后来就不了了之。
最近想学习一下被传得神乎其神的Lisp,这门堪称古董的语言,据说其资格仅次于Fortune。
可能是沿袭K&R写的经典C语言书籍《C Programming Language》的原因。
基本上介绍每一种语言的书籍开篇第一个例子都是hello world。
介绍Lisp方言Scheme的《Teach Yourself Scheme in Fixnum Days》也不例外。
 
下面首先来看看这个hello world:
;the first program
(begin
  (display "hello,world!")
  (newline))
 Scheme中";"表示注释
 Scheme中所有代码均处于圆括号中。
 该段代码以begin开始,begin表示接下来有几条独立的语句。
 如上所示,分别是
 (display "hello,world!")
 该条语句输出"hello world!"
 (newline)
 该条语句输出空行。
 当然输出"hello world!"还可以直接用字符串形式,即:
>"hello world!"
hello world!
>