Мы будем рассматривать сегодня ввод-вывод в языке Scheme R5RS. Язык Scheme R5RS — не промышленный, а академический, поэтому средства ввода-вывода в нём довольно ограничены.
Для сравнения, Common Lisp — промышленный язык, в нём средства ввода-вывода более обширны.
Для абстракции ввода-вывода в Scheme R5RS используется понятие порта. Есть порт ввода и порт вывода по умолчанию, можно создавать новые порты, связанные с файлами.
Порт ввода по умолчанию связан с клавиатурой (stdin
, в терминах языка Си),
порт вывода — с экраном (stdout
, в терминах языка Си). Эти порты по умолчанию
можно переназначать.
Предикат типа «порт»:
(port? x) → #t или #f
Создание порта:
(open-input-file "имя файла") → port
(open-output-file "имя файла") → port
Предусловие для open-output-port
: файл существовать не должен (иначе ошибка).
После использования порты, связанные с файлами, нужно закрывать:
(close-input-port port)
(close-output-port port)
Порты по умолчанию:
(current-input-port) → port
(current-output-port) → port
Временное перенаправление портов:
(with-output-to-file "имя файла" proc) ≡ (proc)
(with-input-from-file "имя файла" proc) ≡ (proc)
Здесь proc
— процедура без параметров, во время выполнения этой процедуры
порты вывода и ввода, соответственно, по умолчанию будут перенаправлены.
Возвращаемое значение у этих функций то же, что у вызванной процедуры.
(with-output-to-file "D:/test.txt"
(lambda ()
(display 'hello)))
В файл D:/test.txt
будет записана строчка hello
.
Открытие портов с автоматическим закрытием:
(call-with-input-file "имя файла" proc) ≡ (proc port)
(call-with-output-file "имя файла" proc) ≡ (proc port)
Здесь функция proc
будет принимать порт в качестве параметра:
(call-with-output-file "D:/test2.txt"
(lambda (port)
(display 'hello port)))
Порт, переданный в процедуру, будет закрыт при завершении вызова самой процедуры.
Для чтения и записи используются функции read-char
, peek-char
,
write-char
:
(read-char) → char | eof-object
(read-char port) → char | eof-object
(peek-char) → char | eof-object
(peek-char port) → char | eof-object
(write-char char)
(write-char char port)
Если порт не указан, то используется порт по умолчанию. Функции read-char
и peek-char
возвращают либо литеру, либо признак конца файла (end-of-file).
Проверка прочитанного на конец файла делается предикатом:
(eof-object? obj) → #t | #f
Функция read-char
читает литеру и забирает его из источника, функция
peek-char
— читает литеру и оставляет её в источнике. Т.е. вызов
(let* ((x (read-char))
(y (read-char))
(z (read-char)))
(list x y z))
вернёт три последовательных символа из порта. Вызов
(let* ((x (peek-char))
(y (peek-char))
(z (peek-char)))
(list x y z))
построит список из трёх одинаковых литер, причём литера останется во входом
порту — её можно будет получить следующим вызовом read-char
или peek-char
.
Можно читать и записывать целые s-выражения, синтаксический анализ будет выполнен библиотекой, а мы получим готовое выражение.
(write expr)
(write expr port)
Функция write
выписывает в порт выражение в машиночитаемом виде. Т.е.
выписанное выражение однозначно понятно. Для чтения машиной записанного
выражения используется функция
(read) → expr | eof-object
(read port) → expr | eof-object
То, что мы записали при помощи write
, мы можем потом прочитать при помощи
read
. Однако, не все данные поддаются чтению обратно.
Снова прочитать мы можем только данные следующих типов:
#t
, #f
,100500
, 2/3
, 3.1415926
, 7+2i
,"Hello!"
,#\H
, #\!
, #\newline
,'hello
,Снова прочитать мы можем только объекты, для которых существуют литералы.
Собственно, функция write
и выписывает данные в виде литералов, а функция
read
их разбирает.
Снова прочитать мы не можем объекты, существующие при выполнении конкретного
процесса: процедуры (lambda
), порты, продолжения (continuations). Для двух
последних литералов не существует. Синтаксис (lambda …)
можно считать
литералом для процедуры, но процедура — вещь существенно динамическая, т.к.
захватывает переменные из своего окружения (см. идиому статических переменных).
Здесь рассмотрим вывод человекочитаемых данных, он представлен двумя функциями:
(display)
(display port)
(newline)
(newline port)
display
выводит данные в человекочитаемом виде. Т.е. строки выводятся не как
свои литералы, а с буквальной интерпретацией символов в них (перевод строки
приведёт к печати перевода строки в файле, а не выдаче литер \
и n
). Вывод
следующих трёх вызовов будет идентичен:
(display #\x)
(display "x")
(display 'x)
По выводу будет непонятно, что же было реальным аргументом. Но, когда пользуются
функцией display
, это и не нужно.
Если для отладки нужно выводить какое-то выражение, то его лучше выводить при
помощи write
, т.к. по выводу будет однозначно понятно содержимое. В частности,
write
следует использовать в макросе trace-ex
и каркасе модульных тестов
в ЛР3 для вывода выражений.
REPL (read-evaluate-print loop) — режим интерактивной работы с интерпретируемыми языками программирования. Пользователь вводит конструкцию языка (выражение, оператор), она тут же интерпретируется и результат выводится на экран. После чего пользователь может снова что-то ввести.
Впервые REPL появился для языка LISP, сейчас он поддерживается многими интерпретаторами языков программирования. Например, среда IDLE в Python, консоль JavaScript, доступная в любом браузере (часто вызывается по F12).
REPL можно реализовать в Scheme самостоятельно:
(define (print expr)
(write expr)
(newline))
(define (REPL)
(let* ((e (read)) ; read
(r (eval e (interaction-environment))) ; eval
(_ (print r))) ; print
(REPL))) ; loop
Добавим поддержку конца файла:
(define (REPL)
(let* ((e (read))) ; read
(if (not (eof-object? e))
(let* ((r (eval e (interaction-environment))) ; eval
(_ (print r))) ; print
(REPL))))) ; loop
Встроенная функция load
позволяет прочитать и проинтерпретировать
содержимое файла:
(load "trace.scm")
(load "unit-tests.scm")
Аналог функции load
можно написать самостоятельно:
(define (my-load filename)
(with-input-from-file filename REPL))
Примечание. Чтобы среда DrRacket не печатала #<void>
для конструкций
без значения в нашем импровизированном REPL’е, функцию print
можем
уточнить:
(define the-void (if #f #f))
(define (print expr)
(if (not (equal? expr the-void))
(begin
(write expr)
(newline))))