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