通过前面章节的学习,你已经可以在Scheme的交互式前端中编写并执行程序了。在本章中,我讲介绍如何输入和输出。使用这个特性,你可以从文件中读取数据或向文件中写入数据。
从文件输入
open-input-file,read-char和eof-object?
函数(open-input-file filename)
可以用于打开一个文件。此函数返回一个用于输入的端口。函数(read-char port)
用于从端口中读取一个字符。当读取到文件结尾(EOF) 时,此函数返回eof-object
,你可以使用eof-object?
来检查。函数(close-input-port port)
用于关闭输入端口。[代码片段1]展示了一个函数,该函数以字符串形式返回了文件内容。
(define (read-file file-name)
(let ((p (open-input-file file-name)))
(let loop((ls1 '()) (c (read-char p)))
(if (eof-object? c)
(begin
(close-input-port p)
(list->string (reverse ls1)))
(loop (cons c ls1) (read-char p))))))
比如,在[范例1]中展示的结果就是将[代码片段1]应用于文件hello.txt。由于换行符是由'\n'
表示的,这就很容易阅读。然而,像格式化输出[范例2],我们也可使用display
函数。
Hello world!
Scheme is an elegant programming language.
(cd "C:\\doc")
(read-file "hello.txt")
;Value 14: "Hello world!\nScheme is an elegant programming language.\n"
(display (read-file "hello.txt"))
Hello world!
Scheme is an elegant programming language.
;Unspecified return value
语法call-with-input-file和with-input-from-file
你通过使用语法call-with-input-file
和with-input-from-file
来打开文件以供读取输入。这些语法是非常方便的,因为它们要处理错误。
(call-with-input-file filename procedure)
该函数将名为
filename
的文件打开以供读取输入。函数procedure
接受一个输入端口作为参数。文件有可能再次使用,因此当procedure
结束时文件不会自动关闭,文件应该显式地关闭。[代码片段1]可以按照[代码片段2]那样用call-with-input-file
编写。
(define (read-file file-name)
(call-with-input-file file-name
(lambda (p)
(let loop((ls1 '()) (c (read-char p)))
(if (eof-object? c)
(begin
(close-input-port p)
(list->string (reverse ls1)))
(loop (cons c ls1) (read-char p)))))))
(with-input-from-file filename procedure)
该函数将名为filename
的文件作为标准输入打开。函数procedure
不接受任何参数。当procedure
退出时,文件自动被关闭。[代码片段3]展示了如何用with-input-from-file
来重写[代码片段1]。
(define (read-file file-name)
(with-input-from-file file-name
(lambda ()
(let loop((ls1 '()) (c (read-char)))
(if (eof-object? c)
(list->string (reverse ls1))
(loop (cons c ls1) (read-char)))))))
read
函数(read port)
从端口port
中读入一个S-表达式。用它来读诸如"paren.txt"中带括号的内容就很方便。
'(Hello world!
Scheme is an elegant programming language.)
'(Lisp is a programming language ready to evolve.)
(define (s-read file-name)
(with-input-from-file file-name
(lambda ()
(let loop ((ls1 '()) (s (read)))
(if (eof-object? s)
(reverse ls1)
(loop (cons s ls1) (read)))))))
下面展示了用s-read
读取"paren.txt"的结果。
(s-read "paren.txt")
⇒ ((quote (hello world! scheme is an elegant programming language.))
(quote (lisp is a programming language ready to evolve.)))
练习1
编写函数
(read-lines)
,该函数返回一个由字符串构成的表,分别代表每一行的内容。在Scheme中,换行符是由#\Linefeed
表示。下面演示了将该函数用于"hello.txt"的结果。
(read-lines "hello.txt") ⇒ ("Hello world!" "Scheme is an elegant programming language.")
输出至文件
打开一个用于输出的port
输出有和输入类似的函数,比如:
(open-output-file filename)
该函数打开一个文件用作输出,放回该输出端口。
(close-output-port port)
关闭用于输出的端口。
(call-with-output-file filename procedure)
打开文件filename
用于输出,并调用过程procedure
。该函数以输出端口为参数。
(with-output-to-file filename procedure)
打开文件filename
作为标准输出,并调用过程procedure
。该过程没有参数。当控制权从过程procedure
中返回时,文件被关闭。
用于输出的函数
下面的函数可用于输出。如果参数port
被省略的话,则输出至标准输出。
(write obj port)
该函数将obj
输出至port
。字符串被双引号括起而字符具有前缀#\
。
(display obj port)
该函数将obj
输出至port
。字符串不被 双引号括起而字符不 具有前缀#\
。
(newline port)
向 port
输出一个换行符。
(write-char char port)
该函数向port
写入一个字符。
练习2
编写函数
(my-copy-file)
实现文件的拷贝。练习3
编写函数
(print-line)
,该函数具有任意多的字符作为参数,并将它们输出至标准输出。输出的字符应该用新行分隔。
小结
因为Scheme的IO设施非常的小,所以本章也十分短。下一章中,我会讲解赋值。
习题解答
答案1
(define (group-list ls sep)
(letrec ((iter (lambda (ls0 ls1)
(cond
((null? ls0) (list ls1))
((eqv? (car ls0) sep)
(cons ls1 (iter (cdr ls0) '())))
(else (iter (cdr ls0) (cons (car ls0) ls1)))))))
(map reverse (iter ls '()))))
(define (read-lines file-name)
(with-input-from-file file-name
(lambda ()
(let loop((ls1 '()) (c (read-char)))
(if (eof-object? c)
(map list->string (group-list (reverse ls1) #\Linefeed)) ; *
(loop (cons c ls1) (read-char)))))))
示例:
(group-list '(1 4 0 3 7 2 0 9 5 0 0 1 2 3) 0)
;Value 13: ((1 4) (3 7 2) (9 5) () (1 2 3))
(read-lines "hello.txt")
;Value 14: ("Hello world!" "Scheme is an elegant programming language." "")
答案2
(define (my-copy-file from to)
(let ((pfr (open-input-file from))
(pto (open-output-file to)))
(let loop((c (read-char pfr)))
(if (eof-object? c)
(begin
(close-input-port pfr)
(close-output-port pto))
(begin
(write-char c pto)
(loop (read-char pfr)))))))
答案3
(define (print-lines . lines)
(let loop((ls0 lines))
(if (pair? ls0)
(begin
(display (car ls0))
(newline)
(loop (cdr ls0))))))
下一节:因为Scheme是函数式语言,通常来说,你可以编写不使用赋值的语句。然而,如果使用赋值的话,有些算法就可以轻易实现了。尤其是内部状态和继续(continuations )需要赋值。
尽管赋值非常习见并且易于理解,但它有一些本质上的缺陷。参见《计算机程序的构造和解释》的第三章第一节“赋值和局部状态”以及《为什么函数式编程如此重要》。
R5RS中规定的用于赋值的特殊形式是set!、set-car!、set-cdr!、string-set!、vector-set!等。除此之外,有些实现也依赖于赋值。由于赋值改变了参数的值,因此它具有破坏性(destructive)。Scheme中,具有破坏性的方法都以!结尾,以警示程序员。