Scheme — это диалект Lisp с маленьким ядром: несколько базовых форм, лексическая область видимости, обязательная оптимизация хвостовых вызовов и продолжения как обычные значения. Язык придумали Гай Стил и Джеральд Сассман в MIT в 1975 году, а сегодня его описывают стандарты серии RnRS, актуальный из них — R7RS.
Содержание
Ниже разберем, какие особенности отличают Scheme от других языков семейства, и каждую покажем на коде. Все примеры проверены 23.09.2026 в chibi-scheme 0.9.1 и GNU Guile 3.0.9 (Ubuntu 24.04).
Три термина, которые путают
Прежде чем смотреть код, разведем понятия, которые в статьях о Scheme часто смешивают.
| Термин | Что это | Пример |
|---|---|---|
| Язык Scheme | Спецификация: синтаксис и семантика | Документ R7RS |
| Стандарт RnRS | Версия спецификации (Revised^n Report) | R5RS (1998), R6RS (2007), R7RS small (2013) |
| Реализация | Программа, которая исполняет код | chibi-scheme, GNU Guile, Chez Scheme, Chicken, Gambit |
| Производный язык | Язык, выросший из Scheme, но со своими правилами | Racket (до 2010 года — PLT Scheme) |
Практический вывод: код из учебника по R7RS обычно запускается в любой реализации, которая заявляет поддержку R7RS. Расширения конкретной реализации (модули Guile, #lang в Racket) переносятся уже не всегда.
Минимальная программа на Scheme
Сначала полный пример, который можно скопировать в файл hello.scm и запустить командой chibi-scheme hello.scm.
(import (scheme base) (scheme write))
(define (square x) (* x x))
(define (factorial n)
(if (= n 0)
1
(* n (factorial (- n 1)))))
(display (square 7)) (newline)
(display (factorial 20)) (newline)
(display (factorial 30)) (newline)
(display (map square '(1 2 3 4))) (newline)
(display (/ 1 3)) (newline)
Результат прогона:
49
2432902008176640000
265252859812191058636308480000000
(1 4 9 16)
1/3
Что здесь видно:
- Префиксная запись. Любой вызов — список в скобках, первым идет операция:
(* x x),(+ 3 5). Операторов с приоритетами нет, поэтому скобки заменяют и приоритет, и разделители. defineзадает и переменную(define имя значение), и функцию(define (имя параметры) тело).- Числа без переполнения. 30! не влезает в 64-битное целое, но Scheme печатает его точно. Деление
(/ 1 3)дает точную дробь1/3, а не0.333. Точные целые произвольной длины и дроби входят в полную числовую башню стандарта; в минимальных реализациях набор типов может быть уже. importподключает библиотеки R7RS:(scheme base)с базовыми формами и(scheme write)с процедуройdisplay.
Guile выполнит тот же файл командой guile hello.scm, но предупредит в stderr, что (scheme base) переопределяет встроенную map. Результат при этом тот же.
Ключевые особенности Scheme
Маленькое ядро и один синтаксис для кода и данных
Код Scheme записан теми же списками, с которыми работает программа. Отсюда простота разбора и макросы, которые получают код как данные. Ядро небольшое: производные формы (let, cond, case и другие) в стандарте определены через примитивные.
Scheme — язык с единым пространством имен (так называемый Lisp-1): функция и переменная живут в одной таблице имен. В Common Lisp пространства раздельные, поэтому там нужен funcall, а в Scheme функцию из переменной вызывают просто (f x).
Лексическая область видимости и замыкания
Имя ищется там, где функция записана в тексте, а не там, откуда ее вызвали. Поэтому функция может «унести» с собой локальную переменную — это замыкание. Блоки задаются формами let, let* и letrec, и разница между ними видна на одном примере.
(import (scheme base) (scheme write))
(define x 10)
(let ((x 1) (y x))
(display (list x y)) (newline))
(let* ((x 1) (y x))
(display (list x y)) (newline))
(letrec ((my-even? (lambda (n) (if (= n 0) #t (my-odd? (- n 1)))))
(my-odd? (lambda (n) (if (= n 0) #f (my-even? (- n 1))))))
(display (my-even? 100)) (newline))
(define (make-counter)
(let ((count 0))
(lambda ()
(set! count (+ count 1))
count)))
(define c (make-counter))
(c) (c)
(display (c)) (newline)
(1 10)
(1 1)
#t
3
Разбор по строкам вывода:
| Форма | Когда видны привязки | Результат в примере |
|---|---|---|
let |
Все значения вычисляются до связывания, y видит внешний x |
(1 10) |
let* |
Последовательно, y видит только что связанный x |
(1 1) |
letrec |
Привязки видят друг друга, нужно для взаимной рекурсии | #t |
| Замыкание | count живет, пока жива возвращенная функция |
3 |
Хвостовая рекурсия гарантирована стандартом
Главная практическая особенность. Вызов в хвостовой позиции (последнее действие функции, после которого ничего не надо досчитывать) не наращивает стек. Стандарт R7RS требует этого от любой реализации, поэтому циклы в Scheme пишут рекурсией, а do в стандарте тоже есть, но используется реже.
(import (scheme base) (scheme write))
(define (sum-to n)
(let loop ((i 1) (acc 0))
(if (> i n)
acc
(loop (+ i 1) (+ acc i)))))
(display (sum-to 10)) (newline)
(display (sum-to 10000000)) (newline)
55
50000005000000
Форма (let loop ...) называется named let: она создает локальную функцию loop и сразу ее вызывает. Вызов (loop ...) стоит в хвостовой позиции, сумма копится в аккумуляторе acc, и десять миллионов итераций проходят в постоянной памяти стека.
Для сравнения — та же сумма без аккумулятора, где после рекурсивного вызова еще нужно сложить n:
(import (scheme base) (scheme write))
(define (sum-naive n)
(if (= n 0)
0
(+ n (sum-naive (- n 1)))))
(display (sum-naive 10000000)) (newline)
Здесь поведение зависит от реализации, и это важная граница. chibi-scheme 0.9.1 завершилась с ошибкой:
ERROR: out of stack space
GNU Guile 3.0.9 напечатал 50000005000000: он умеет расширять стек по мере надобности, пока хватает памяти. Стандарт гарантирует постоянную память только для хвостовых вызовов; нехвостовая рекурсия расходует память пропорционально глубине в любой реализации, различается лишь предел. Исправление — перенести отложенную работу в аккумулятор, как в sum-to.
Продолжения: call/cc
Продолжение — это «остаток вычисления» в данной точке программы. Процедура call-with-current-continuation (сокращенно call/cc) передает его в функцию как обычное значение. Если вызвать это значение, программа сразу вернется в точку захвата. Простейшее применение — досрочный выход из обхода списка.
(import (scheme base) (scheme write))
(define (find-first pred lst)
(call-with-current-continuation
(lambda (return)
(for-each (lambda (x)
(when (pred x) (return x)))
lst)
#f)))
(display (find-first even? '(1 3 8 5 10))) (newline)
(display (find-first even? '(1 3 5))) (newline)
8
#f
В первом списке for-each доходит до 8, вызов (return 8) прерывает обход, и 10 уже не проверяется. Во втором четных нет, и функция возвращает #f. Это упрощенная демонстрация: тем же механизмом в Scheme строят исключения, генераторы и сопрограммы. В прикладном коде для ошибок удобнее guard и raise из R7RS, а call/cc оставляют для таких конструкций.
Гигиенические макросы
Макрос через syntax-rules описывает шаблон преобразования кода. Гигиена означает, что внутренние имена макроса не конфликтуют с именами пользователя.
(import (scheme base) (scheme write))
(define-syntax swap!
(syntax-rules ()
((_ a b)
(let ((tmp a))
(set! a b)
(set! b tmp)))))
(define tmp 1)
(define other 2)
(swap! tmp other)
(display (list tmp other)) (newline)
(2 1)
Внутри макроса есть своя переменная tmp, и пользователь передал переменную с тем же именем. Обмен все равно прошел верно: макрос-система переименовала внутреннее имя.
Типичная ошибка: лишняя скобка
Большая часть ошибок новичка в Scheme — скобки. Неверный код с преждевременно закрытым if:
(import (scheme base) (scheme write))
(display
(let loop ((n 1))
(if (> n 10))
'()
(cons n (loop (+ n 1)))))
(newline)
chibi-scheme 0.9.1 сообщает (строки трассировки ниже опущены):
ERROR on line 5 of file ex6.scm: not enough args to if: (if (> n 10))
Guile 3.0.9 пишет иначе:
Syntax error:
unknown location: source expression failed to match any pattern in form (if (> n 10))
Исправление — закрывающая скобка переезжает в конец if, чтобы обе ветки оказались внутри:
(import (scheme base) (scheme write))
(display
(let loop ((n 1))
(if (> n 10)
'()
(cons n (loop (+ n 1))))))
(newline)
(1 2 3 4 5 6 7 8 9 10)
Обратите внимание: (cons n (loop ...)) — не хвостовой вызов, cons выполняется после возврата. Для десяти элементов это неважно, для миллионов лучше накопить список в аккумуляторе и развернуть его через reverse.
Реализации Scheme в 2026 году
Выбор реализации зависит от задачи: учеба, встраивание в программу или скорость.
| Задача | Что взять | Особенности |
|---|---|---|
| Учеба по SICP и R7RS | chibi-scheme, Guile | Маленькие, ставятся из пакетов Linux |
| Учебная среда с IDE | Racket (DrRacket) | Отдельный язык-наследник, Scheme-диалекты через #lang |
| Скриптинг и встраивание в C-программы | GNU Guile | Официальный язык расширений GNU, на нем описывают пакеты GNU Guix |
| Быстрый нативный код | Chez Scheme | С версии 8.0 основа Racket CS |
| Компиляция в C | Chicken, Gambit, Bigloo | Удобно для утилит и связки с C-библиотеками |
Где Scheme используют на практике
Самая известная роль Scheme — обучение. Книга «Structure and Interpretation of Computer Programs» (SICP) построена на Scheme, и по ней до сих пор учат основам программирования и устройству интерпретаторов. Сам курс MIT, для которого книгу писали, позже перешел на Python.
Вне учебы Scheme встречается как язык расширений (Guile в GNU-проектах, Guix), как основа Racket и как язык исследований в области компиляторов и языков программирования. Массового прикладного рынка вакансий именно под Scheme нет: идеи языка обычно переносят в работу на других функциональных языках.
Выводы
- Scheme — минималистичный диалект Lisp, актуальный стандарт R7RS; язык, стандарт и реализация — три разные вещи.
- Хвостовые вызовы по стандарту не расходуют стек, поэтому циклы пишут рекурсией с аккумулятором; глубина нехвостовой рекурсии упирается в предел конкретной реализации.
- call/cc делает продолжения значениями и позволяет строить досрочный выход, исключения и генераторы.
- Лексическая область видимости дает замыкания, а
syntax-rules— гигиенические макросы без конфликтов имен. - Для первых шагов хватит chibi-scheme или Guile из пакетов Linux и книги SICP.
Где применяется / связь с практикой
Scheme редко становится рабочим языком, но его приемы напрямую переносятся в прикладные Lisp-диалекты. Ближайший пример — Clojure на JVM: те же скобки и код как данные, неизменяемые структуры, функции высшего порядка и макросы. Отличается модель циклов: JVM не дает гарантированной оптимизации хвостовых вызовов, поэтому в Clojure для этого есть явная форма recur.
Освойте тему на практике
Если хочется применить функциональный подход в продакшене, посмотрите программу «Clojure Developer». Чтобы сначала познакомиться с форматом занятий, загляните на бесплатные открытые уроки Otus.
FAQ
Чем Scheme отличается от Common Lisp?
Scheme меньше и строже стандартизирован по ядру, имеет единое пространство имен и гарантирует хвостовые вызовы. Common Lisp — большой промышленный стандарт с раздельными пространствами имен, CLOS и негигиеническими макросами defmacro.
Racket — это Scheme?
Racket вырос из PLT Scheme и сохранил многое из него, но это самостоятельный язык со своим стандартом и библиотеками. Код R5RS и R6RS в нем запускается через соответствующие #lang, поддержка R7RS ставится отдельным пакетом.
Что такое R7RS-large?
Это продолжение R7RS small с большой стандартной библиотекой. Работа над ним ведется по частям, поэтому для переносимого кода сегодня ориентируются на R7RS small.



