Scheme 编程/输入和输出
一个文件本质上只是存储在您的计算机硬盘驱动器(或 USB 闪存盘、SD 卡或其他存储设备)上的一个字符串,并且带有名称。硬盘驱动器上还有一种带有名称的物体类型,那就是目录。“目录”是程序员一直用来称呼后来被称为“文件夹”的东西。它们是文件名列表。
当您想在 Scheme 中处理文件内容时,您使用诸如read-char以及write-char之类的函数,它们一次以一个字节的方式从文件检索或添加字符。或者,您可以使用库函数,例如read-line或read以及write,它们都一次读取或写入多个字符,并以不同的方式解析数据。
在您能够从文件读取或向文件写入之前,您必须打开它。这意味着从操作系统获取一个文件描述符,它是一个跟踪您在文件中的位置的值——下一个读取的字符将是文件中的哪个字符,或者下一个写入的字符将在文件中的什么位置结束。在 Windows 上,文件描述符还为您提供了对文件的排他写入权限——其他程序不能写入您正在写入的同一个文件或删除它。但是,Unix(Linux 和 Mac OS)不提供此类保证。
端口是 Scheme 的文件描述符值。它们被传递给输入/输出过程,以告知 I/O 函数从哪个文件读取或写入哪个文件。每个端口代表一个文件和一个方向。也就是说,一个端口可以是输入端口或输出端口,但不能同时是两者。
键盘和屏幕或终端也是文件。通常,它们是同一个文件/dev/tty在 Unix 上,或CON用于 Windows 控制台程序。但是,您的 Web 浏览器等 GUI 程序通常没有与它们关联的控制台或 TTY(代表电传打字机)。
在 Scheme REPL 中,代表键盘和电传打字机的文件默认情况下是打开的。它有三个端口(current-input-port)用于键盘,(current-output-port)用于电传打字机,以及(current-error-port),也用于电传打字机,用于错误消息。通常将错误消息发送到(current-error-port)而不是(current-output-port)。这是因为有可能重定向这些端口。例如,current-output-port 可以重定向到一个文件,而错误消息仍然会打印在屏幕上。
> (current-input-port)
#<input-output-soft 9a8ad18>
> (current-output-port)
#<input-output-soft 9a8ad18>
> (current-error-port)
#<output-port /dev/pts/20>
> (display "This is a string" (current-output-port))
This is a string#<unspecified>
>
display 函数不会在其输出后打印换行符。它会从字符串中删除引号,但否则会以与在 Scheme 源代码中找到的相同的格式显示 Scheme 值。
要打印换行符,请使用newline 函数
> (begin
(display "This is a string" (current-output-port))
(newline (current-output-port)))
This is a string
#<unspecified>
端口参数实际上是可选的。如果您不包含它,display
、newline 和其他输入和输出 (I/O) 函数将假设您是指输出的
(current-output-port)
或输入的 (current-input-port)
。
您可以使用以下任一方法打开文件open-input-file进行读取,或open-output-file写入文件。这些函数返回的值是一个端口,需要绑定到某样东西。
> (define in (open-input-file "test.c")) ; Just a C source file I have lying around.
#<unspecified>
> (read-line in)
"#include <stdio.h>"
> (close-input-port in)
0
>
在您完成文件操作后,关闭端口非常重要。您可以同时打开的文件数量有限制。此限制是由操作系统而不是 Scheme 强加的。
以输出模式打开文件会擦除其内容。
> (define out (open-output-file "test.c"))
#<unspecified>
> (display "Problem?" out) ; My C source file is wiped out and replaced with this. >:(
#<unspecified>
> (newline out)
#<unspecified>
> (close-output-port out)
0
当您将字符写入文件时,某些 Scheme 实现不会真正写入它们,而是将它们存储在内部缓冲区中,直到收到足够的字节或写入换行符为止。当您关闭文件时,将写入任何剩余的缓冲字符。
操作系统会在 Scheme 退出时自动关闭所有文件。
在某些 Scheme 实现中,open-output-file如果文件已存在,则会引发错误。例如,在 Racket 上
> (define out (open-output-file "test.c"))
open-output-file: file exists
path: /tmp/test.c
context...:
/usr/share/racket/collects/racket/private/misc.rkt:87:7
>
Scheme 提供了read函数,它从端口读取并解析 Scheme 值(或(current-input-port)如果没有指定端口),以及对应的write函数。这是将数据输入和输出 Scheme 的最简单方法,因此,只要可行,Scheme 程序员就喜欢将他们的数据存储为 Scheme 代码。例如,假设您有以下文件
just-some-raw-data.scm
((0.00036277727 0.00024514514 0.00010899892 -0.00017201288 5.1782848e-05) (0.000252906 0.00015007147 -0.00023179696 -0.00037388649 8.3796775e-05) (-0.00037429505 -0.00020174753 0.00043324157 0.00015203918 0.0003337927) (0.0001250037 5.5220273e-05 -0.00049933029 -0.00010911703 -0.00019316927) (0.00018089121 4.254036e-05 0.00018602787 -2.7271702e-05 -0.00024643468))
您可以编写一个程序来读取文件,对其中的所有数字做一些操作,并将结果写回同一个文件
manipulate-raw-data.scm
(define filename "just-some-raw-data.scm")
(define in (open-input-file filename))
(define raw-data (read in))
(close-input-port in)
(define out (open-output-file filename))
(write (map (lambda (row)
(map (lambda (num)
(* num 100000)) row)) raw-data)
out)
(close-output-port out)
然后,在 REPL 中
> (load "manipulate-raw-data.scm")
; loading manipulate-raw-data.scm
; done loading manipulate-raw-data.scm
#<unspecified>
>
该文件的新的内容将是
just-some-raw-data.scm
((36.27772699999999 24.514513999999998 10.899892 -17.201288 5.1782847999999974) (25.290600000000003 15.007147 -23.179696000000005 -37.388649 8.3796775) (-37.429505000000005 -20.174753 43.324157 15.203918 33.379269999999996) (12.500370000000003 5.5220273000000004 -49.933029 -10.911703 -19.316927) (18.089121000000002 4.254036 18.602787 -2.7271701999999997 -24.643468000000003))
请注意,Scheme 不会以人类易于阅读或看起来不错的格式格式化输出。但是,如果您使用同一个程序加载此文件,它将毫无困难地读取这些值并再次更改它们。
read-line 函数从端口读取一行文本(或(current-input-port)如果没有指定)。当在 REPL 中使用时,读取通常从读取代码的同一行开始
> (define (prompt/read prompt)
> (display prompt)
> (read-line))
#<unspecified>
> (prompt/read "Enter your name: ")
Enter your name: ""
如您所见,Scheme 甚至没有给用户输入名称的机会。但是
> (prompt/read "Enter your name: ") Johnny Boy
Enter your name: " Johnny Boy"
发生这种情况是因为 Scheme 看到右括号后就停止读取。read-line会看到之后的任何内容。如果它从文件中加载,则不会影响您的程序。
> (delete-file "test.c")
Scheme 提供了with-input-from-file以及with-output-to-file函数,它们接受一个函数作为参数。他们会重定向(current-input-port)或(current-output-port)以便它们在指定的文件上打开,然后调用您提供的函数,然后在该函数退出时关闭文件。然后,您可以调用程序中的任何函数,如果它们写入(current-output-port)或从(current-input-port)读取,那么这些函数也将从该文件读取/写入该文件。
在某些 Scheme 实现中,即使发生错误,文件也会关闭,这很重要,因为在 R5RS Scheme 中没有办法捕获错误(但是,各种 Scheme 实现提供了扩展来允许捕获错误,而在某些实现中,with-input-from-file不捕获错误)。不必定义端口变量也很不错。
上面的文件操作程序可以使用with-input-from-file以及with-output-to-file编写。然后程序将如下所示
manipulate-raw-data.scm
(define filename "just-some-raw-data.scm")
(define raw-data (with-input-from-file filename read))
(with-output-to-file filename
(lambda ()
(write (map (lambda (row)
(map (lambda (num)
(* num 100000)) row)) raw-data))))
某些 Scheme 实现提供with-input-from-string,它会重定向(current-input-port)就像with-input-from-file一样。但是,SCM 仅提供call-with-input-string,它类似于with-input-from-string,除了您提供的过程必须接受端口作为参数。
> (define my-string "the quick brown fox jumps over the lazy dog\n")
#<unspecified>
> (call-with-input-string my-string (lambda (port) (values (read port) (read port))))
the
quick
还可以像写入端口一样写入字符串。call-with-output-string用于此。字符串从头开始创建。这是将任何值转换为字符串的一种方法。
> (call-with-output-string
(lambda (out)
(write (sqrt 2) out)))
"1.4142135623730951"
原始二进制数据在不同的 Scheme 实现之间并非 100% 可移植。一些实现提供 SRFI-56,它提供read-byte, write-byte, peek-byte和byte-ready?。如果你的 Scheme 实现没有提供它们,并且它的字符采用单字节编码(如 ASCII)并且没有使用 Unicode,你可以自己定义它们。
(define (read-byte . opt)
(let ((c (apply read-char opt)))
(if (eof-object? c) c (char->integer c))))
(define (write-byte int . opt)
(apply write-char (integer->char int) opt))
(define (peek-byte . opt)
(let ((c (apply peek-char opt)))
(if (eof-object? c) c (char->integer c))))
(define byte-ready? char-ready?)
然后,字节可以使用 OR(bitwise-ior)、AND(bitwise-and)和位移(arithmetic-shift或ash)进行组合。以下函数用于将字节列表转换为整数,假设使用“大端”编码,这在网络数据包中很常见。
(define (big-endian->integer list)
(let loop ((list list)
(result 0)
(shift (* 8 (- (length list) 1))))
(if (null? list)
result
(loop (cdr list)
(bitwise-ior result (arithmetic-shift (car list) shift))
(- shift 8)))))
你可以用它从端口读取任意大小的整数。
(define (read-big-endian-integer bytes . port)
(let loop ((bytes bytes)
(result '()))
(if (= bytes 0)
(big-endian->integer (reverse result))
(loop (- bytes 1)
(cons (apply read-byte port) result)))))
要读取小端,这是 Intel CPU 本地使用的格式,只需不要反转结果即可。拥有一个可以读取两种格式的函数可能很方便。然后你可以在它上面定义这两种读取函数。
(define (read-binary-integer bytes maybe-reverse . port)
(let loop ((bytes bytes)
(result '()))
(if (= bytes 0)
(big-endian->integer (maybe-reverse result))
(loop (- bytes 1)
(cons (apply read-byte port) result)))))
(define (read-big-endian-integer bytes . port)
(apply read-binary-integer (append (list bytes reverse) port)))
(define (read-little-endian-integer bytes . port)
(apply read-binary-integer (append (list bytes identity) port)))
Scheme 提供了identity函数,它只返回它的参数,专门用于像上面这样的情况,我们使用它是因为在读取小端时我们不想反转结果或对结果进行任何其他操作。
在二进制文件中,字符串存储为以数据开头的二进制长度,或者以空字符结尾的字符串。在二进制长度的情况下,长度本身可以具有不同的长度,并且可以是小端或大端字节序。下面的函数需要包含所有这些信息的参数。
(define (read-counted-string count-size-in-bytes byte-order . port)
(let ((string-size (case byte-order
((big-endian) (apply read-big-endian-integer (cons count-size-in-bytes port)))
((little-endian) (apply read-little-endian-integer (cons count-size-in-bytes port))))))
(let loop ((result '())
(remaining-bytes string-size))
(if (= remaining-bytes 0)
(list->string (reverse result))
(loop (cons (apply read-char port) result)
(- remaining-bytes 1))))))
(define (read-null-terminated-string . port)
(let loop ((result '()))
(let ((next-char (apply read-byte port)))
(if (= next-char 0)
(reverse result)
(loop (cons (char->integer next-char) result))))))
最后,拥有一个可以从文件读取整个结构的函数可能很方便。你可以将结构的格式指定为一个列表,该列表包含要读取的整数的大小,并指定何时期望一个字符串。例如,你可以调用(read-binary '(big-endian 2 4 (counted 1)))读取一个 16 位大端整数,然后是一个 32 位整数,最后是一个长度由 8 位整数表示的计数字符串。
(define (read-binary spec . port)
(define (read-integer byte-order size)
(case byte-order
((big-endian) (apply read-big-endian-integer (cons size port)))
((little-endian) (apply read-little-endian-integer (cons size port)))))
(let loop ((spec spec)
(endian 'big-endian)
(result '()))
(cond ((null? spec)
(reverse result))
((eq? (car spec) 'big-endian)
(loop (cdr spec) 'big-endian result))
((eq? (car spec) 'little-endian)
(loop (cdr spec) 'little-endian result))
((list? (car spec))
(case (caar spec)
((counted) (loop (cdr spec)
endian
(cons (apply read-counted-string
(append (list (cadr (car spec)) endian) port))
result))
(null-term) (loop (cdr spec)
endian
(cons (apply read-null-terminated-string port) result)))))
((number? (car spec))
(loop (cdr spec) endian (cons (read-integer endian (car spec)) result)))
(else
(error "Invalid token:" (car spec))))))
上面的读取函数假设所有整数都是无符号的,这意味着没有办法表示负数。但是,你可能在二进制文件中找到的一些整数旨在被解释为“有符号的”。假设你有一个有符号字节,你将其读取为无符号字节。无符号字节是 8 位,可以表示从 0 到 255 的值。任何大于该值的值都需要超过 8 位才能存储。一个有符号字节可以使用与无符号字节完全相同的格式表示从 0 到 127 的值,但无符号字节中解释为 128 的值在有符号字节中是 -128。无符号 129 映射到 -127,依此类推,直到你得到无符号 255,它映射到 -1。
以下函数将任何大小(以字节为单位)的无符号整数转换为有符号整数。
(define (unsigned->signed number orig-size)
(let ((max (inexact->exact (- (floor (/ (expt 2 (* orig-size 8)) 2)) 1))))
(if (> number max)
(- number (* 2 (+ 1 max)))
number)))