[ИУ9] Основы информатики

Лекция 9а. Ввод-вывод в языке Scheme

Мы будем рассматривать сегодня ввод-вывод в языке 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. Однако, не все данные поддаются чтению обратно.

Снова прочитать мы можем только данные следующих типов:

Снова прочитать мы можем только объекты, для которых существуют литералы. Собственно, функция 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 (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))))