Scheme/Tutorial/4
Четвёртая часть рассказа.
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, которой передаются два параметра: список и среда исполнения. Что такое второй параметр -- не будем пока заморачиваться, а примем как данность.
Исполнитель рекурсивно пробегается по всем вложенным спискам, большинство констант интерпретируются в них самих (строки в строки, числа в числа), а вот наткнувшись на символ, производится поиск соотв. переменной и в результате подставляется то на что она ссылается (Note: здесь описана так называемая подстановочная модель, она не точная, зато простая и понятная, то что происходит в реальном интерпретаторе -- гораздо сложнее, но суть остаётся примерно та же)
После того как все переменные разобраны, если в результате остаётся список, то происходит запуск функции. Первый элемент списка - собственно указатель на саму функцию, например в одном из наших примеров - это стандартная функция 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.