Ввод и вывод

Ввод и вывод входят в диалог

  До сих пор в определённых нами функциях ввод данных и вывод осуществлялись в процессе диалога с интерпретатором. Интерпретатор читал вводимое пользователем выражение, вычислял его значение и возвращал его пользователю. Сами формы и функции не содержали ничего, связанного с вводом или выводом.
  Если не использовать специальную команду ввода, то данные можно передавать функции только через параметры и свободные переменные. Соответственно, без использования вывода, результат можно получить лишь через конечное значение выражения. Часто всё же возникает необходимость вводить исходные данные и выдавать сообщения и тем самым управлять и получать промежуточные результаты во время вычислений, как это делается и в других языках программирования.

READ-LINE читает и возвращает выражение

  Функция чтения read-line отличается от ввода в других языках программирования тем, что она обрабатывает выражение целиком, а не одиночные элементы данных. Вызов этой функции осуществляется пользователем в виде

  (cin read-line)

  Как только интерпретатор встречает команду read-line, вычисления приостанавливаются до тех пор, пока пользователь не введёт какой-нибудь символ или целиком выражение:

>(cin read-line)
(вводимое выражение) ; выражение пользователя
"(вводимое выражение)" ; значение функции read-line

  Обратите внимание, read-line никак не показывает, что он ждёт ввода выражения. Программист должен сам сообщить об этом при помощи рассматриваемых позже функций вывода. read-line лишь читает выражение и возвращает в качестве значения само это выражение как строку, после чего вычисления продолжаются.
  У приведённого выше вызова функции read-line не было аргументов, но у этой функции есть значение, которое совпадает с введённым выражением. По своему действию read-line представляет собой функцию, но у неё есть побочный эффект, состоящий именно во вводе выражения. Учитывая это, read-line является не чистой функцией, а псевдофункцией.
  Если прочитанное значение необходимо сохранить для дальнейшего использования, то вызов read-line должен быть аргументом какой-нибудь формы, например присваивания, которая свяжет полученное выражение:

>(nil setq  input (cin  read-line))
(2 + 3) ; введённое выражение
nil ; значение
>input
"(2 + 3)" ; строка

  Данная функция и форма, вызывающая интерпретатор read совместно с другими функциями позволяют читать выражения внешние по отношению к программе. Из них можно строить новые выражения или целые программы. Построенные структуры можно вычислить, передав их непосредственно интерпретатору:

>(input read) ; интерпретатор
(2  + 3)  ; список
>(nil eval  (input  read))
5

Программа ввода выделяет формы

  Функция read основана на работающей на системном уровне процедуре чтения. Она читает s-выражение, из знаков, поступающих из файла или иного источника. Внешние устройства становятся доступными из Лисп-системы через объекты, называемые потоками. На логическом уровне потоки независимо от характера внешнего устройства являются списками читаемых или записываемых знаков или битов. Для ввода и вывода, как и для двустороннего обмена, существуют свои типы потоков и специальные операции.

Символы хранятся в векторе объектов

  Читая и интерпретируя знаки, процедура чтения пытается строить атомы и из них списки. Прочитав имя символа, интерпретатор ищет, встречался ли ранее такой символ или он неизвестен. Для нового символа нужно зарезервировать память для возможного значения, определения функции и других свойств. Символы сохраняются в памяти в векторе объектов, в котором они проиндексированы на основании своего имени. Вектор объектов содержит как созданные пользователем, так и внутрисистемные символы (например, first, cons, nil и т.д.). Внесение символа в вектор объектов называют включением или интернированием.

Пакеты или пространства имён

  Можно пользоваться несколькими различными векторами объектов, которые называют пространствами имён. Символы из различных пространств, имеющие одинаковые имена, могут использоваться различным способом. Это необходимо при построении больших систем, при программировании различных её подсистем программисты частенько используют одинаковые имена для различных целей. Все глобальные переменные интерпретатора определены в пространстве имён std. На переменные из других пространств имён можно сослаться, используя метод namespace.

WRITELN печатает значение и переходит на новую строку

  Для вывода выражений можно использовать функцию writeln. Эта функция сначала вычисляет значения аргументов, а затем выводит их значения. Функция writeln после печати аргументов переходит на новую строку.

>(cout  writeln (2  + 3))
5 ; вывод (эффект)
STREAM:Stdout ; значение
>(cout  writeln (cin  read-line))
(2 + 3) ; ввод
(2 + 3) ; вывод
STREAM:Stdout ; значение

  Как и read-line, writeln является псевдофункцией, у которой есть как побочный эффект, так и значение. Значением функции является поток, а побочным эффектом - печать.
  Операторы ввода-вывода, как и присваивания, очень гибки, поскольку их можно использовать в качестве аргументов других функций, что в других языках программирования обычно невозможно:

>((cout writeln 2)  writeln 3)
2
3
STREAM:Stdout

PRINC и WRITE выводят без перевода строки

  write работает так же, как writeln, но не переходит на новую строку:

>(nil progn (cout writeln 1)  (cout write 2)  (cout write 3))
1
23

  Функцией princ можно выводить кроме атомов и списков и другие типы данных, например строки, представляемые знаками, заключённых с обеих сторон в кавычки ("). Вывод в такой форме позволяет процедуре чтения (read) вновь прочесть выведенное выражение в виде, логически идентичном первоначальному. Таким образом, строка выводится вместе с ограничителями:

>(cout  princ "a b c")
"a b c"

  Более приятный вид с точки зрения пользователя можно получить при помощи функции write. Она выводит объекты в том же виде как и princ, но преобразует некоторые типы данных в более простую форму. Такие выведенные выражения нельзя прочесть (read) и получить выражения, логически идентичные выведённым. Функцией write мы можем напечатать строку без ограничивающих её кавычек и специальные знаки без их выделения:

>(cout  write "a b c")
a b c ; вывод без кавычек

  С помощью функции write можно, естественно, напечатать и скобки:

>(nil progn (cout write "(((")
  (cout princ 'луковица)
  (cout write ")))"))
(((луковица)))
STREAM:Stdout

  Функция вывода возвращает значение потока.

TERPRI переносит строку

  Вывод выражений и знаков часто желательно разбить на несколько строк. Перенос строки можно осуществить функцией writeln, которая автоматически переносит строку после вывода, или непосредственно для этого предназначенной функцией terpri (terminate printing). У функции terpri нет аргументов и в качестве значения она возвращает поток:

>(nil progn (cout princ 'a)
  (cout terpri)
  (cout writeln 'b)
  (cout princ 'c))
a
b
c
STREAM:Stdout ; значение

  Метод writeln можно определить с помощью terpri и write следующим образом:

>('stream defmethod writeln (x)
  (this write x)
  (this terpri))
(lambda (x) (this write x)  (this terpri))

Использование файлов

  Ввод и вывод во время работы с Лиспом в основном осуществляется через дисплей и клавиатуру. Файлы используются, как правило, лишь для хранения программ в промежутке между сеансами. Однако иногда возникает необходимость работы с файлами из программы.
  Ввод и вывод осуществляются независимо от конфигурации внешних устройств через потоки (stream). Потоки представляют собой специальные резервуары данных, из которых можно читать (поток ввода) или в которые можно писать (поток вывода) знаки или двоичные данные. У системы есть несколько стандартных потоков, которые являются значениями глобальных переменных. Наиболее важные из них

  cin и
  cout

  Эти системные переменные определяют для функций ввода (read-line и др.) и соответственно для функций вывода (prinx, terpri и др.) файлы по умолчанию, которыми в начале сеанса являются дисплей или терминал пользователя. В этом случае нет необходимости в начальном объявлении потока ввода для функции read-line или потока вывода для функций вывода.
  Если мы хотим обмениваться данными с каким-нибудь новым файлом, то сначала его нужно открыть (open) в зависимости от его использования для чтения или для записи. У директивы open может быть много параметров.

  (имя-файла open {параметр})

  Значением параметра могут быть следующие ключи:

параметрзначение
inОткрыть поток для ввода
outОткрыть поток для вывода

  Например, следующий вызов открывает поток для вывода в файл проба.lisp2d:

("проба.lisp2d" open 'out)

  Потоковый объект, получаемый в результате вызова open, можно для следующего использования присвоить какой-нибудь переменной:

(nil  setq  поток
  ("проба.lisp2d"  open  'in 'out  'trunc))

  Этот вызов присваивает переменной поток двусторонний поток, связанный с новым файлом "проба.lisp2d".
  Все ранее рассмотренные функции ввода и вывода можно использовать и с указанием файла, задавая соответствующий файлу поток. Например, следующий вызов princ записывает выражение в новый файл "проба.lisp2d", и вызов read читает это выражение:

>(поток  princ '(в  файл  "проба.lisp2d"))
STREAM:IO
>(поток  seekr 0)
STREAM:IO
>(nil setq  x (поток read))
nil
>x
(в файл  проба.lisp2d)

  Чтобы записанные в файл данные сохранились, надо не забыть закрыть файл директивой close:

  (поток close)

LOAD загружает определения

  На практике написание программ осуществляется записью в файл определений функций, данных и других объектов с помощью имеющегося в программном окружении редактора. После этого для проверки определений вызывают интерпретатор Лиспа, который может прочитать записанные в файл выражения директивой load:

  (имя-файла load)

  Читаемые выражения вычисляются так, как будто бы они были введены пользователем. После загрузки можно использовать функции, значения переменных, значения свойств и другие определения.
  Для работы с файлами содержится кроме описанных выше средств большое количество возможностей, в том числе переименование и удаление файлов.