Если вы когда-нибудь задумывались, каково это — играть в бога с программированием, добро пожаловать в Racket — здесь создание собственного языка программирования не только возможно, но и активно поощряется. Это не типичный язык программирования уровня «Hello, World!», где вы проводите месяцы, пытаясь просто вывести текст. Racket — это универсальный инструмент для создания языков, и сегодня мы подробно разберём, почему он заслужил звание «языка для создания языков».
Что делает Racket особенным?
Racket похож на того невероятно талантливого друга, который не любит хвастаться. Это современный диалект Lisp и Scheme, который незаметно революционизирует представления о дизайне языков программирования с начала 2000-х годов. Пока другие языки спорят о необходимости точек с запятой, Racket непринуждённо создаёт целые парадигмы программирования.
Секрет в том, что Racket рассматривает код как данные, а данные как код — концепция, которая может показаться философской абракадаброй, пока вы не поймёте, что это ключ к магии создания языков. Эта гомоиконичная природа (модное слово для «кода, который может изменять себя») означает, что вы можете буквально изменять язык по мере его написания.
Настройка вашей языковой лаборатории
Прежде чем мы начнём создавать языки, как будто мы строим замки из LEGO, давайте установим Racket. Процесс удивительно прост — никаких зависимостей, никаких кошмаров с конфигурацией, только чистое удовольствие от установки.
Шаги по установке
Для пользователей Linux (культурный выбор):
# Ubuntu/Debian
sudo apt-get install racket
# Arch Linux (потому что вы, вероятно, используете Arch, кстати)
sudo pacman -S racket
# Fedora
sudo dnf install racket
Для других платформ перейдите на racket-lang.org
, нажмите «Скачать» и следуйте инструкциям.
После установки вы получите DrRacket — среду разработки, которая как тёплый hug для программистов. Она достаточно продвинута для экспертов, но достаточно проста, чтобы ваша бабушка, вероятно, могла написать в ней компилятор (хотя она могла бы спросить, почему вы заставляете компьютер использовать так много скобок).
Ваш первый опыт с Racket
Запустите DrRacket, и вы увидите чистый интерфейс. Выберите «Racket» в качестве языка, и давайте напишем нашу первую программу:
#lang racket
"Hello, World of Language Creation!"
Нажмите «Выполнить», и поздравляю — вы только что выполнили свою первую программу на Racket. Обратите внимание на строку #lang racket
? Это не просто украшение; это буквально сообщает системе, какой язык вы используете. Хотите использовать другой язык? Измените эту строку. Хотите создать свой собственный язык? Вы можете сделать и это, и мы это сделаем.
Основы Racket: принятие скобок
Racket использует префиксную нотацию, что означает, что функция идёт первой, за ней следуют её аргументы. Это как говорить Йодой, но для компьютеров:
#lang racket
; Базовые арифметические операции — всё является вызовом функции
(+ 2 3) ; Возвращает 5
(* 4 5) ; Возвращает 20
(- 10 3) ; Возвращает 7
; Функции могут принимать несколько аргументов
(+ 1 2 3 4 5) ; Возвращает 15
; Списки являются фундаментальными
(list 1 2 3 4)
'(a b c d) ; Список с кавычками (не вычисляет содержимое)
; Определение переменных
(define answer 42)
(define pi 3.14159)
; Определение функции
(define (square x)
(* x x))
(square 5) ; Возвращает 25
Прелесть этого синтаксиса в его последовательности. Всё следует одному и тому же шаблону: (имя-функции аргумент1 аргумент2 ...)
. Никаких особых случаев, никакой памятки о приоритете операторов — просто красивая математическая простота.
Работа со списками и данными
Списки — это основа Racket (или, если хотите, скобки и пробелы). Вот как ими манипулировать:
#lang racket
; Создание списков
(define numbers '(1 2 3 4 5))
(define languages (list "Racket" "Python" "JavaScript" "Rust"))
; Доступ к элементам списка
(first numbers) ; Возвращает 1
(rest numbers) ; Возвращает '(2 3 4 5)
(length numbers) ; Возвращает 5
; Классические car и cdr (first и rest в старом Лиспе)
(car numbers) ; То же, что и first
(cdr numbers) ; То же, что и rest
; Манипуляция списками
(append '(1 2) '(3 4)) ; Возвращает '(1 2 3 4)
(reverse numbers) ; Возвращает '(5 4 3 2 1)
; Функции высшего порядка (здесь происходит волшебство)
(map square numbers) ; Применяет square к каждому элементу
(filter even? numbers) ; Возвращает только чётные числа
(foldl + 0 numbers) ; Суммирует все числа
Управление потоком: функциональный способ
Racket предпочитает рекурсию циклам, как математик, который настаивает на доказательстве всего из первых принципов:
#lang racket
; Условные выражения
(define (describe-number n)
(cond
[(= n 0) "ноль"]
[(positive? n) "положительное"]
[(negative? n) "отрицательное"]))
; Выражения if (всё возвращает значение)
(define (absolute-value x)
(if (< x 0)
(- x)
x))
; Рекурсивные функции (циклы — это так мейнстримно)
(define (factorial n)
(if (<= n 1)
1
(* n (factorial (- n 1)))))
; Хвостовая рекурсия (эффективный вид)
(define (factorial-tail n acc)
(if (<= n 1)
acc
(factorial-tail (- n 1) (* n acc))))
(define (factorial-better n)
(factorial-tail n 1))
Создание вашего первого предметно-ориентированного языка
Теперь основное событие — создание собственного языка программирования. Мы собираемся построить простой язык калькулятора, который будет более выразительным, чем базовая арифметика, но проще, чем полный Racket.
Шаг 1: Определите синтаксис вашего языка
Давайте создадим язык под названием «calc», который поддерживает переменные и базовые операции:
#lang racket
; Файл: calc.rkt
; Это будет наша реализация языка
(provide (rename-out [calc-read read]
[calc-read-syntax read-syntax]))
(define (calc-read in)
(syntax->datum
(calc-read-syntax #f in)))
(define (calc-read-syntax src in)
(define line (read-line in))
(if (eof-object? line)
eof
(with-syntax ([body (parse-calc-line line)])
#'(module anonymous calc
body))))
(define (parse-calc-line line)
; Простой парсер для нашего языка calc
(define tokens (string-split line))
(cond
[(equal? (first tokens) "let")
`(define ,(string->symbol (second tokens))
,(string->number (third tokens)))]
[(equal? (first tokens) "add")
`(+ ,(string->symbol (second tokens))
,(string->number (third tokens)))]
[else
`(displayln "Неизвестная команда")]))
Шаг 2: Создайте среду выполнения языка
; Файл: calc/lang/reader.rkt
#lang s-exp syntax/module-reader
calc/main
; Файл: calc/main.rkt
#lang racket
(provide (all-defined-out))
; Среда выполнения нашего языка
(define-syntax-rule (calc-module body ...)
(#%module-begin
body ...))
(provide (rename-out [calc-module #%module-begin]))
; Встроенные функции для нашего языка
(define (show-result x)
(printf "Результат: ~a\n" x))
(define