Scheme — один из самых чистых диалектов Lisp: маленький синтаксис, лексические замыкания, гарантированная оптимизация хвостовых вызовов и гигиенические макросы. На нём приятно учить функциональное программирование и писать небольшие утилиты, которые не тянут за собой гигабайты зависимостей. В статье — практический старт на GNU Guile: установка, REPL, списки, рекурсия, работа с файлами, тесты и компиляция.

Почему Scheme, а не Common Lisp

Common Lisp — большой промышленный язык с множеством встроенных форм и библиотек. Scheme сознательно минималистичен: в основе несколько специальных форм, а остальное вынесено в стандартные библиотеки SRFI. Это делает язык предсказуемым и удобным для встраивания. Guile дополнительно используется как язык расширения в GnuCash и других проектах GNU, поэтому навык пригодится не только в учебных задачах.

Стандарт R7RS определяет переносимый набор возможностей; реализации добавляют свои модули. Код на чистом R7RS будет работать в Guile, Racket, Chicken и Chez с минимальными правками.

Установка Guile

В Debian и Ubuntu пакет называется guile-3.0, в Fedora — guile30. После установки проверьте версию и запустите простейшее выражение.

sudo apt install guile-3.0
guile --version
guile -c '(begin (display (+ 1 2)) (newline))'
guile

Последняя команда открывает REPL. В нём можно вводить выражения и сразу видеть результат; выход — (exit) или Ctrl+D. REPL — главный инструмент отладки: не нужно создавать файл, чтобы проверить одну функцию.

База: define, lambda, let

Определение функции — это define с параметрами. Тело может состоять из нескольких выражений, возвращается последнее.

(define (square x) (* x x))
(define cube (lambda (x) (* x x x)))
(define (hypotenuse a b)
  (sqrt (+ (square a) (square b))))
(display (hypotenuse 3 4))
(newline)

let создаёт локальные привязки, let* позволяет ссылаться на предыдущие, letrec — на взаимно рекурсивные функции. set! меняет существующую переменную, но злоупотреблять им не стоит: код с мутациями сложнее тестировать. В функциональном стиле результат собирают из возвращаемых значений, а не изменяют общий контейнер.

Списки и рекурсия

Список строится из пар (car . cdr), где car — первый элемент, а cdr — остаток. Пустой список обозначается '(). Основные операции: cons, car, cdr, null?, pair?.

(define xs '(1 2 3 4 5))
(display (car xs))
(display (cdr xs))
(display (map square xs))
(display (filter odd? xs))
(newline)

Хвостовая рекурсия в Scheme не растёт по стеку, поэтому цикл принято записывать через именованный let. Это идиоматичнее, чем do, и легче читается.

(define (sum-to n)
  (let loop ((i 0) (acc 0))
    (if (> i n)
        acc
        (loop (1+ i) (+ acc i)))))
(display (sum-to 100))
(newline)

Практический скрипт: частота слов

Соберём небольшой анализатор текста. Он читает файл построчно, разбивает строки на слова и считает повторы в хеш-таблице. Для работы со списками и строками подключим SRFI-1, SRFI-13 и SRFI-14.

#!/usr/bin/guile -s
!#
(use-modules (ice-9 rdelim)
             (ice-9 format)
             (srfi srfi-1)
             (srfi srfi-13)
             (srfi srfi-14))

(define (read-lines path)
  (call-with-input-file path
    (lambda (port)
      (let loop ((line (read-line port)) (acc '()))
        (if (eof-object? line)
            (reverse acc)
            (loop (read-line port) (cons line acc)))))))

(define (count-words path)
  (let ((table (make-hash-table)))
    (for-each
     (lambda (line)
       (for-each
        (lambda (word)
          (hash-set! table word (1+ (hash-ref table word 0))))
        (string-tokenize line char-set:letter)))
     (read-lines path))
    table))

(hash-for-each
 (lambda (word count) (format #t "~a ~a~%" word count))
 (count-words "text.txt"))

Запуск: guile words.scm. Шебанг #!/usr/bin/guile -s и строка !# позволяют сделать файл исполняемым и запускать его как обычный скрипт. read-line возвращает объект конца файла, а не пустую строку, поэтому проверка идёт через eof-object?.

Обратите внимание на hash-ref с третьим аргументом: это значение по умолчанию для отсутствующего ключа. Без него программа упала бы на первом же новом слове. Такая мелочь типична для Scheme: библиотека не угадывает намерения, а требует явности.

Модули и тесты

Код удобно выносить в модуль. Файл words.scm объявляет экспорт, а основной скрипт подключает его через use-modules. Для тестов подойдёт SRFI-64: он не требует внешних зависимостей.

;; words.scm
(define-module (words)
  #:export (count-words))

;; test-words.scm
(use-modules (srfi srfi-64) (words))
(test-begin "words")
(test-equal 4 (+ 2 2))
(test-end "words")

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

Компиляция и альтернативы

Guile компилирует байткод в .go-файлы и кэширует их. Для утилиты, которая запускается часто, имеет смысл собрать файл заранее.

guild compile -o words.go words.scm
guile words.scm
chmod +x words.scm
./words.scm

Если нужен самодостаточный бинарник, посмотрите на Chicken: csc -O2 hello.scm -o hello собирает нативный исполняемый файл. Racket предлагает мощную среду и raco exe, Chez Scheme славится скоростью запуска. Guile хорош балансом: быстрый старт, нормальная стандартная библиотека и тесная интеграция с GNU-системой.

Не ждите от Scheme экосистемы Python. Библиотек меньше, часть задач придётся решать через SRFI, FFI или вызов внешних утилит. Зато язык остаётся маленьким, а поведение программы предсказуемым: синтаксис и область видимости не меняются от контекста, как в языках с множеством неявных правил.

Чек-лист

  • Установлен Guile, REPL запускается, версия видна.
  • Функции определены через define, локальные привязки — через let.
  • Циклы записаны хвостовой рекурсией, а не глубокой вложенностью.
  • Для списков подключены SRFI-1, для строк — SRFI-13.
  • Скрипт имеет шебанг и проверен запуском из терминала.
  • Есть хотя бы минимальные тесты SRFI-64.

Итог

Scheme — язык, который учит думать о данных и функциях, а не о синтаксическом сахаре. На Guile можно за вечер написать парсер логов, преобразователь файлов или небольшой генератор отчётов, не устанавливая ничего лишнего. Начните с REPL и хвостовой рекурсии, затем добавьте модули и тесты. Минимализм здесь не ограничение, а способ держать всю программу в голове — качество, которое в больших языках встречается всё реже.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте