Scheme/Tutorial/4

Материал из ALT Linux Wiki

Четвёртая часть рассказа.

9 Открываем капот.

Может быть, из академических соображений так поступать не стоит, однако иногда бывает проще рассказать про причину, чем про её следствия. Давайте разберёмся с тем, как работает интерпретатор Scheme, благо знания нам уже это позволяют сделать. После этого всё, что было до сего момента неясно, уже станет очевидным.

Вот основной цикл работы интерпретатора:

  • прочитать очередное выражение
  • провести интерпретацию этого выражения

Если мы находимся в командном интерпретаторе, то после последнего этапа ещё происходит печать получившегося выражения.

Вот, собственно, и всё! Теперь разберёмся с каждым шагом. Как и многое другое, процесс интерпретации разделён на несколько отдельных функций, что позволяет контролировать его с точностью до миллиметра — если это, конечно, надо.

Рассмотрим выражения:

3
"string"
(+ 1 3)
(string-append "str1" "str2")
(some-func a #t (+ 1 3))
a
9.1 Чтение

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

Первое — константа — число 3
Второе — константа — строка "string"
Третье — список из констант:

  • символ + (вот они, символы-то! неспроста они в языке существуют ;))
  • число 1
  • число 3

Четвёртое — сами, наверное, уже догадались.
Пятое — список из:

  • константа — символ some-func
  • константа — символ a
  • константа — логическая не ложь.
  • ещё один список из: символа + и чисел 1 и 3.

Шестое — символ a.

Вот вам и весь парсер ;)

9.2 Исполнение

Исполнить полученный список можно с помощью функции eval, которой передаются два параметра: список и среда исполнения. Что такое второй параметр — не будем пока заморачиваться, а примем как данность.

Исполнитель рекурсивно пробегается по всем вложенным спискам, большинство констант интерпретируются в них самих (строки в строки, числа в числа) — а вот наткнувшись на символ, производится поиск соответствующей переменной и в результате подставляется то, на что она ссылается[1].

После того, как все переменные разобраны, если в результате остаётся список — то происходит запуск функции. Первый элемент списка — собственно указатель на саму функцию, например, в одном из наших примеров — это стандартная функция string-append, а все оставшиеся — это аргументы функции. Вычисляем указанную функцию от данных аргументов (в том же примере — это две константные строки "str1" и "str2") …и получаем результат исполнения.

Пройдёмся по нашим примерам:

3 — собственно, 3 и получим, никаких функций запускать не надо.
"string" — получим строку.

(+ 1 3) — выполним функцию сложения от аргументов 1 и 3, результат — число 4.

(string-append "str1" "str2") — результат — строка "str1str2".

(some-func a #t (+ 1 3)) — получим результат выполнения функции от аргументов:

  • значение переменной a
  • логическая не ложь
  • результат сложения 1 и 3, то есть аргумент равен 4.

a — значение переменной a

9.3 Вывод результата на экран

Для вывода результата на экран есть множество функций; самая интересная из них это, пожалуй, write.

Если в результате вы получили простую структуру, состоящую из списков (возможно, вложенных) и каких-либо констант (строк, чисел, символов), то write напечатает их в таком виде, что потом read может их обратно съесть.

Те, кто знаком с языками типа Java, узнают в этом знакомые вещи, называемые словами сериализация и маршалинг …только сделанные лет за дцать до этого ;)

Константы будут выведены естественным образом:
7 напечатается как 7
"str" напечатается как "str"
Списки опять напечатаются как знакомые выражения, окруженные скобками; например, список из 1 2 и 3 будет напечатан как (1 2 3)

9.4

Ну а теперь повторим пройденное. Помните про функцию quote, которая позволяла заполучить символы? quote просто-напросто говорит интерпретатору, что не надо исполнять eval после read.

Поэтому 'a — это просто символ a
'(1 2 3) — это список из 1 2 3

10 Особые формы

Аргументы функции обрабатываются в некотором недокументированном порядке — зависит от реализации Схемы.

В отдельных случаях хочется заранее знать, как и когда будут аргументы вычислены. Поскольку эти «функции» такие особые, то и называются они «особые формы»; вот основные, которые нам потребуются на практике.

10.1 Условные выражения

(if условие команда-если-истина команда-если-ложь)

присутствие команда-если-ложь традиционно необязательно.

Сначала вычисляется условие; если оно истинно (то есть не #f), то вычисляется команда-если-истина, иначе вычисляется команда-если-ложь.

Например,

  • (if #f 3 4) — вернёт 4
  • (if (+ 1 3) 5 6) — вернёт 5, поскольку результат (+ 1 3), то есть 4 — «не ложь».
  • (if (string=? "aaa" "bbb") 3 5) — вернёт 5, ибо строки не равны
  • (if (number? 5) "number" "not number") — вернёт строку "number" , ибо 5 — действительно число, а не что другое ;)
  • (if #f 3) — поскольку "команда-если-ложь" отсутствует, то if вернёт некоторое волшебную сущность, которую иногда называют unspecific, иногда unspecified — в общем «то, не знаю что» или «неопределенное значение» — особый вид значения.
10.2 Множественное ветвление

Если в данной точке программы надо исследовать множество различных вариантов, то используйте cond

Формат: (cond вариант1 вариант2 ... )

вариант оформляется в виде (тест выражение1 выражение2 ... )

есть ещё специальный вариант (else выражение1 выражение2 ... ), который применяется, если никакой другой вариант не прошёл. else может отсутствовать и должен быть всегда последним вариантом в случае присутствия.

Например, давайте попробуем понять, а что же пришло в функцию: строка или число, символ или что-то ещё?

(define (func x)
  (cond
    ((string? x) "строка")
    ((number? x) "число")
    ((symbol? x) "символ")
    (else "не знаю, что такое")))
10.3 Последовательное исполнение

Иногда в if допустима только одна команда на условие истины и одна на условие лжи. А что делать, если хочется исполнить сразу много команд в случае некоторого условия? Иногда можно, конечно, обойтись имеющимися средствами — но гораздо проще воспользоваться особой формой begin, которая вычисляет свои аргументы строго последовательно слева направо. В качестве результата возвращается результат работы последнего выражения.

Пример:

(begin (+ 1 2)
     (+ 3 4))

сначала сложит 1 и 2, потом сложит 3 и 4 и вернёт в качестве результата 7.

далее>>

  1. здесь описана так называемая подстановочная модель, она неточная, зато простая и понятная; то, что происходит в реальном интерпретаторе — гораздо сложнее, но суть остаётся примерно та же