Введение
Язык Scheme (произносится "ским"), которому в следующем году исполняется 50 лет, является языком программирования, занимающим необычное место среди прочих. Это язык, который гораздо больше изучают, чем потом на нём пишут. Скорее это язык для развития ума программиста, чем для написания коммерческого кода, хотя и примеры использования Scheme в коммерческой разработке тоже встречаются. На мой личный взгляд, Scheme идеален в качестве первого языка программирования в старшем школьном и институтском возрасте, а также идеально продолжает изучение Scratch в младших классах школы и Logo в средних классах.
Достоинства Scheme:
– предельно простой синтаксис, позволяющий не отвлекаться на изучение бессмысленных синтаксических деталей;
– небольшая по описанию, но очень мощная по выразительным средствам и глубочайше формализованная семантика;
– прекрасный учебник Массачусетского технологического института по конструированию программ "Структура и интерпретация компьютерных программ" SICP, переведённый на русский язык, использующий язык Scheme;
– интерактивная среда разработки и динамическая структура языка, позволяющие не забивать голову программиста лишними проблемами и наглядно видеть результаты каждого шага программирования;
– возможность очень легко писать программы, расширяющие свой собственный исходный текст, что является одним из методов символического искусственного интеллекта.
Недостатки Scheme:
– язык редко используется в коммерческой разработке и имеет мало прикладных библиотек и фреймворков;
– к синтаксису языка с большим количеством скобочек нужно привыкнуть;
– программы в обычном стиле Scheme имеют огромную плотность смыслового содержания по отношению к длине их текста, в связи с чем их чтение человеком затруднено (а оплата за строки кода невыгодна);
– относительно небольшое сообщество.
В общем, если вы хотите привести в порядок своё понимание основных концепций программирования (или просто блеснуть академическими познаниями), учите Scheme. Если ваша работа связана с решением алгоритмически сложных задач, то вы, возможно, даже найдёте этому языку практическое применение.
Что такое Scheme?
Scheme – это диалект языка Lisp, имеющий некоторые глубокие, но не очень многочисленные отличия и потому выделенный в отдельный язык. Если вы понимаете Lisp, то без труда поймёте и Scheme, и наоборот.
Стандарт языка Scheme регламентируется документами, носящими в лучших традициях префиксной функциональной записи названия Revised Revised ... Revised Report on the Algorithmic Language Scheme, или просто R*RS. На сегодняшний день основные реализации поддерживают стандарты от R5RS до R7RS. Документ R7RS имеет объём 88 страниц, в которых полностью описаны синтаксис и семантика языка, начиная от формального математического определения семантики в лямбда-исчислении.
Также важной частью фактически используемого языка Scheme являются Scheme Requests for Implementation, или SRFI, представляющие собой предлагаемые сообществом дополнения к стандарту. Все SRFI пронумерованы, в настоящий момент их предложено ровно 255 штук. Реализации Scheme включают тот или иной набор SRFI “из коробки”, интегрированными во входной язык. Но это не так важно, так как особенностью Scheme является расширяемость входного языка его же средствами, поэтому большинство SRFI представлены макрокомандами, которые можно использовать в своих программах независимо от их наличия в дистрибутиве транслятора.
Реализаций языка Scheme очень много, многие из них входят в стандартные репозитории Linux. Заслуживают упоминания реализации MIT Scheme (она используется в SICP) и Racket (удобная графическая пользовательская среда). Сам автор пользуется реализацией Gambit Scheme, так как её интерпретатор и компилятор имеют легко собираемый исходный код без внешних зависимостей, а также в полной мере поддерживают национальные языки в идентификаторах (в Linux для этого может потребоваться пересборка). Последнее важно для создания на базе Scheme пользовательских предметно-ориентированных языков. Gambit Scheme (пока в экспериментальном режиме) поддерживает подключение и вызов библиотек для языка Python.
Наконец, необходимо отметить, что написание программ на Scheme (и других диалектах Лиспа) рано или поздно приведёт вас к редактору emacs.
Далее приведём несколько элементарных примеров кода на Scheme, ни в коем случае не имея в виду соревноваться с SICP.
Напишем программу Hello world
"Hello world!"
"Hello world!"
Вычислим 1/3 + 1/7
(+ 1/3 1/7)
10/21
Здесь мы видим, что основным элементом синтаксиса языка являются выражения в скобках (S-выражения), которые, если их интерпретировать как данные, представляют собой списки, а если их интерпретировать как код, представляют собой формы функциональных вызовов с функцией на первой позиции и её аргументами на остальных позициях. Фундаментальным отличительным свойством Scheme (и других диалектов Лиспа) является гомоиконичность, то есть одинаковое представление кода и данных. Отметим, что этим свойством обладает также машинный код, а в других языках программирования оно утрачено.
Вычислим 100!
(letrec
((fact (lambda (n f)
(if (zero? n)
f
(fact (- n 1) (* f n))))))
(fact 100 1))
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
Фактически единственным способом организации повторяющихся вычислений в Scheme, как и в лямбда-исчислении, является рекурсия. Стандарт Scheme гарантирует оптимизацию хвостовой рекурсии. Имеется в языке и свой цикл do, но он является макросом, реализуемым через хвостовую рекурсию.
Отсортируем строки в текстовом файле
(list-sort
string<=?
(call-with-input-file
"myfile.txt"
(lambda (p) (read-all p read-line))))
Обратите внимание, что нам пока что не пришлось определить ни одной функции, чтобы потом её вызывать. Со всеми этими простыми задачами мы справлялись одним вычислимым выражением.
Напишем функцию, которая из списка чего попало делает строку
(define (anylist->string args)
(string-concatenate
(map (lambda (x)
(if (string? x)
x
(object->string x)))
args)
" "))
(anylist->string '(1 2 "Hello" 3. #t))
"1 2 Hello 3. #t"
Вызовем ту же самую функцию в отдельной нитке
(thread
(lambda ()
(display
(anylist->string '(1 2 "Hello" 3. #t)))))
(мы вынуждены применить здесь функцию display, потому что результат выполнения из другой нитки сам по себе не напечатается).
Напечатаем значение, которое получило имя нашей функции
anylist->string
#<procedure #2 anylist->string>
(видно, что это какой-то код).
Напечатаем покрасивее
(pp anylist->string)
(lambda (args)
(string-concatenate (map (lambda (x) (if (string? x) x (object->string x))) args) " "))
(а здесь уже видно конкретное функциональное значение).
Создадим список, представляющий собой исходный текст программы, вычисляющей значение 2+2, и выполним эту программу
(eval (list '+ 2 2))
4
(в целом, если в вашей программе осмысленно используются вызовы eval (интерпретация данных как кода), то скорее всего за Scheme или Lisp вы взялись не зря).
Реализуем в языке оператор (если ... то ... иначе ...) и значения "истина" и "ложь":
(define-syntax если
(syntax-rules (то иначе)
((если p то x иначе y) (if p (begin x) (begin y)))
((если p то x) (when p x))))
(define истина #t)
(define ложь #f)
(если истина то "Привет")
"Привет"
Если вас заинтересует язык Scheme, то надо просто брать SICP и читать. На 6-й странице вы научитесь вычислять арифметические выражения, на 129-й – программировать символьное дифференцирование, на 329-й – писать интерпретатор, а на 524-й – писать компилятор.