Особенности Racket
Особенности Racket — особенности языка программирования Racket. Racket активно развивается с середины 1990-х годов как средство для исследований в области языков программирования и за это время приобрёл множество возможностей. Эта статья описывает и демонстрирует некоторые из них. Одной из главных целей разработки Racket является поддержка создания новых языков программирования, как предметно-специфических, так и совершенно новых языков[1]. Поэтому некоторые из приведённых ниже примеров написаны на разных языках, однако все они реализованы в среде Racket. Подробности представлены в основной статье.
Базовая реализация Racket весьма гибкая. Даже без использования диалектов она может служить полноценным скриптовым языком, способным работать как с собственным графическим интерфейсом пользователя (GUI) «под Windows», так и без него, выполнять задачи от создания веб-серверов до работы с графикой.
Поддержка во время выполнения
Racket поддерживает три различных типа сборщиков мусора:
- Изначально использовался консервативный сборщик мусора Бёма. Однако для долгоживущих процессов, таких как веб-сервер, консервативный сборщик непрактичен — такие процессы склонны к медленной утечке памяти. Есть паталогические случаи, когда утечка происходит настолько быстро, что выполнение некоторых программ становится невозможным. Например, при обходе бесконечного списка, одна ошибка консервативного сборщика (удержание указателя) приводит к хранению всего списка в памяти, что быстро исчерпывает ресурсы. В сообществе Racket этот сборщик часто называют «CGC».
- SenoraGC — альтернативный консервативный сборщик мусора, предназначенный в первую очередь для отладки и трассировки памяти.
- Перемещающий сборщик памяти (также известный как «3m») — точный (прецизионный) сборщик мусора, используемый по умолчанию с 2007 года. Является поколенческим сборщиком, поддерживает учёт памяти посредством «кустодианов». Этот сборщик реализован как преобразователь исходного кода на языке C, написанный на самом Racket, поэтому в процессе сборки для задач бустрэппинга применяется консервативный сборщик.
Как и все реализации семейств Scheme, Racket поддерживает полное устранение хвостовых вызовов. Более того, система реализует полную пространственную безопасность благодаря анализу живых переменных, что, вкупе с точной сборкой мусора, является критически важным для некоторых реализаций (например, Lazy Racket). Также язык поддерживает дополнительные оптимизации компилятора, такие как вынос лямбда выражений и компиляцию «на лету» (JIT).
Системный интерфейс Racket включает асинхронные неблокирующие операции ввода-вывода, зелёные потоки, каналы синхронизации, семафоры, подпроцессы и сокеты TCP.
Следующая программа запускает «эхо-сервер» на порту 12345.
#lang racket
(define listener (tcp-listen 12345))
(let echo-server ()
;; создание TCP-сервера
(define-values (in out) (tcp-accept listener))
;; обработка подключения в (зелёном) потоке
(thread (λ () (copy-port in out) (close-output-port out)))
;; немедленный возврат к ожиданию новых клиентов
(echo-server))
Сочетание динамической компиляции и развитого системного интерфейса делают Racket мощным скриптовым языком, аналогичным Perl или Python.
Следующий пример иллюстрирует обход дерева каталогов, начиная с текущей директории. Функция in-directory формирует последовательность для обхода дерева, for связывает path с каждым элементом, а regexp-match? проверяет, соответствует ли путь регулярному выражению.
#lang racket
;; Поиск исходников Racket во всех подкаталогах
(for ([path (in-directory)]) ; обход текущего дерева
(when (regexp-match? #rx"[.]rkt$" path)
(printf "source file: ~a\n" path)))
В следующем примере используется хеш-таблица для записи уже встречавшихся строк и вывода только уникальных.
#lang racket
;; Вывести каждую уникальную строку из stdin
(let ([saw (make-hash)])
(for ([line (in-lines)])
(unless (hash-ref saw line #f)
(displayln line))
(hash-set! saw line #t)))
Обе эти программы можно запускать как в среде DrRacket, так и в командной строке через исполняемый файл racket. Racket игнорирует начальную строку шебанг, что позволяет превращать такие программы в исполняемые скрипты. Пример скрипта, использующего библиотеку для разбора командных строк:
#!/usr/bin/env racket
#lang racket
(command-line
#:args (base-dir ext re)
(for ([p (in-directory)]
#:when (regexp-match? (string-append "[.]" ext "$") p)
[(line num) (in-indexed (file->lines p))])
(when (regexp-match? (pregexp re) line)
(printf "~a:~a: ~a~n" p (+ num 1) line))))
Этот скрипт является утилитой, подобной grep, и ожидает три аргумента: базовую директорию, расширение файлов и регулярное выражение (совместимо с Perl). По указанной директории производится поиск файлов с данным суффиксом и выводятся строки, удовлетворяющие паттерну.
В Racket существует понятие «кустодиан» — значения, служащего менеджером ресурсов. Это часто используется, например, в сетевых серверах: каждое подключение обрабатывается в новом кустодиане, что облегчает очистку всех ресурсов, оставшихся после обработчика (например, закрытие неосвобождённых портов). Пример расширения «эхо-сервера» с использованием кустодиана:
#lang racket
(define listener (tcp-listen 12345))
;; обработка подключения
(define (handler in out)
(copy-port in out)
(close-output-port out))
(let echo-server ()
(define-values (in out) (tcp-accept listener))
(thread (λ ()
(let ([c (make-custodian)])
(parameterize ([current-custodian c])
(handler in out)
(custodian-shutdown-all c)))))
(echo-server))
Кустодианы, совместно с учётом расхода памяти точного сборщика (3m), а также рядом дополнительных параметров времени выполнения, позволяют создавать полностью безопасные среды изолированного исполнения (sandbox). Библиотека racket/sandbox облегчает это. Пример — REPL-сервер, запускающий REPL на определённом порту, но выполняющий код с защитой: из этой сессии нельзя получить доступ к файловой системе, создавать сетевые соединения, запускать процессы или использовать избыточно много памяти/времени.
#lang racket
(require racket/sandbox)
(define e (make-evaluator 'racket/base))
(let-values ([(i o) (tcp-accept (tcp-listen 9999))])
(parameterize ([current-input-port i]
[current-output-port o]
[current-error-port o]
[current-eval e]
[current-read-interaction (λ (x in) (read in))])
(read-eval-print-loop)
(fprintf o "\nBye...\n")
(close-output-port o)))
Веб- и сетевое программирование
Следующий пример реализует мини-веб-сервер через язык web-server/insta. При каждом подключении вызывается функция start, возвращающая HTML, отправляемый клиенту.
#lang web-server/insta
;; Минималистичный веб-сервер «hello world»
(define (start request)
(response/xexpr '(html (body "Hello World"))))
Racket также включает функции для написания парсеров и роботов. Пример ниже выводит результаты поиска Google по строке запроса.
#lang racket
;; Простой веб-скрапер
(require net/url net/uri-codec)
(define (let-me-google-that str)
(let* ([g "http://www.google.com/search?q="]
[u (string-append g (uri-encode str))]
[rx #rx"(?<=<h3 class=\"r\">).*?(?=</h3>)"])
(regexp-match* rx (get-pure-port (string->url u)))))
Также поддерживаются протоколы, отличные от http:
#lang racket
;; Отправка почтового напоминания по времени
(require net/sendmail)
(sleep (* (- (* 60 4) 15) 60)) ; подождать 3 ч 45 мин
(send-mail-message
(getenv "EMAIL") "Parking meter alert!"
(list (getenv "EMAIL")) null null
'("Time to go out and move the car."))
Графика
Графические возможности представлены в разных вариантах, рассчитанных на разные аудитории. Библиотека 2htdp/image предоставляет простые функции для создания изображений, активно используемые студентами в курсах по книге How to Design Programs (HtDP). В примере ниже функция sierpinski создаёт и сразу же вызывает себя, строя треугольник Серпинского глубины 8.
#lang racket
;; Изображение
(require 2htdp/image)
(let sierpinski ([n 8])
(if (zero? n)
(triangle 2 'solid 'red)
(let ([t (sierpinski (- n 1))])
(freeze (above t (beside t t))))))
В редакторе DrRacket можно вставлять изображения, и значения-изображения отображаются в окне как обычные — как числа или списки. Запуск такой программы отображает треугольник Серпинского, который можно скопировать в другой код.
Библиотека plot — для более продвинутых нужд. Следующий пример строит сумму двух трёхмерных функций Гаусса, отображая концентрические полупрозрачные поверхности:
#lang racket
;; Визуализация суммы двух 3D-Гауссианов в виде изоповерхностей
;; Требуется Racket 5.2 и новее
(require plot)
;; Возвращает трёхмерную функцию Гаусса с центром в (cx,cy,cz)
(define ((gaussian cx cy cz) x y z)
(exp (- (+ (sqr (- x cx)) (sqr (- y cy)) (sqr (- z cz))))))
;; Подъём + к трёхаргументным функциям
(define ((f3+ g h) x y z) (+ (g x y z) (h x y z)))
;; Создаёт значение-изображение — сумму двух Гауссианов
(plot3d (isosurfaces3d (f3+ (gaussian 0 0 0) (gaussian 1.5 -1.5 0))
-1 2.5 -2.5 1 -1 1
#:label "g")) ; Добавление легенды
Здесь функция isosurfaces3d требует на вход трёхаргументную функцию, что и делает каррированный f3+. Библиотека plot может записывать изображения в форматы PNG, PDF, PostScript и SVG.
В Racket реализован переносимый слой графического интерфейса, на котором работают вышеупомянутые библиотеки. Он реализован через стандартный API Windows, Cocoa для macOS и GTK+ для Linux и других систем. API Racket для GUI — объектно-ориентированный набор классов, отчасти схожий с wxWidgets, который использовался ранее.
Далее приведён пример простейшей игры — угадывания числа. Класс frame% реализует главное окно, button% — кнопку, check — функцию обратного вызова по нажатию.
#lang racket/gui
;; Игровое окно: угадай число
(define secret (random 5))
(define f (new frame% [label "Guessing game"])) ; главное окно
(define t (new message% [parent f]
[label "Can you guess the number I'm thinking about?"]))
(define p (new horizontal-pane% [parent f])) ; горизонтальный контейнер
(define ((make-check i) btn evt)
(message-box "." (cond [(< i secret) "Too small"]
[(> i secret) "Too big"]
[else "Exactly!"]))
(when (= i secret) (send f show #f))) ; правильный ответ — закрытие окна
(for ([i (in-range 10)]) ; создать все кнопки
(make-object button% (format "~a" i) p (make-check i)))
(send f show #t) ; показать окно
GUI может быть реализован вручную либо с помощью визуального редактора, доступного на PLaneT[2].
Слайды презентаций могут создаваться в Racket на языке slideshow, аналогично как с Beamer в LaTeX, но с применением программных возможностей Racket. Элементы каждого слайда — изображения, которые можно комбинировать.
В следующем примере в полноэкранном режиме выводятся титульный слайд и слайд с изображениями. Функции vc-append и hc-append вертикально и горизонтально объединяют картинки, центрируя их по другой оси.
#lang slideshow
(slide
(text "Slideshow" 'roman 56)
(text "Making presentations in Racket"
'roman 40))
(slide
#:title "Some pictures"
(apply vc-append
(for/list ([i 5])
(define (scale+color p c)
(colorize (scale p (/ (add1 i) 5)) c))
(hc-append
(scale+color (filled-rectangle 100 50) "darkblue")
(scale+color (disk 100) "darkgreen")
(scale+color (arrow 100 (/ pi 6)) "darkred")
))))
Расширения также доступны на PLaneT[2], например, для интеграции элементов LaTeX.
Внешний интерфейс (FFI)
В Racket имеется FFI, основанный на libffi. Этот интерфейс позволяет писать небезопасный низкоуровневый код в стиле языка C — выделять память, разыменовывать указатели, вызывать функции из динамических библиотек, а также передавать обратные вызовы из Racket (используя замыкания libffi). Базовая реализация — тонкий слой над libffi (написан на C), а остальной интерфейс реализован на Racket. В интерфейсе широко используются макросы, что позволяет описывать интерфейсы весьма выразительно. Язык описания интерфейсов поддерживает, в частности, единообразную работу с функциями высшего порядка, определение структур, сходных с обычными структурами Racket, пользовательские типы функций (например, для обработки указателей на вход/выход, неявных аргументов и др.). Посредством этого интерфейса Racket реализует собственный слой GUI[3].
FFI применяется для создания как полноценного «моста» к библиотеке (как в случае реализации OpenGL), так и для быстрого доступа к отдельным функциям. Пример последнего случая:
#lang racket/base
;; Пример простого использования FFI
(require ffi/unsafe)
(define mci-send-string
(get-ffi-obj "mciSendStringA" "Winmm"
(_fun _string [_pointer = #f] [_int = 0] [_pointer = #f]
-> [ret : _int])))
(mci-send-string "play sound.wav wait")
Расширения языка
Важнейшая особенность Racket — возможность создавать новые предметно-специфические и общего назначения языки программирования. Это достигается благодаря следующим возможностям:
- гнучкой модульной системе, используемой для связывания кода и управления пространством имён,
- мощной макросистеме — фактически предоставляющей API компилятора для создания новых синтаксических форм,
- развитой системе времени выполнения с возможностями для авторов новых языков (компонуемые, ограниченные продолжения, менеджмент ресурсов и др.),
- средствам для описания и реализации парсеров новых синтаксисов.
Модульная система важна для объединения этих возможностей и позволяет писать код, охватывающий множество модулей, каждый из которых может быть реализован на своём языке.
Новые языки активно используются как в самом дистрибутиве Racket, так и во внешних библиотеках. Создание полноценного языка настолько упрощено, что есть даже языки, созданные ради нескольких экспериментов.
Racket поставляется с набором языков, некоторые из которых весьма существенно отличаются от основной версии.
Система документации Racket — Scribble — реализована в виде целого семейства языков для написания текста. Она используется для всей документации Racket, а также для написания книг и статей. По сути, Scribble — это не один язык, а семейство схожих диалектов, каждый со своей областью применения.
Для запуска приведённого примера его можно скопировать в DrRacket и воспользоваться одной из кнопок рендеринга Scribble (PDF требует pdfTeX). Также возможно вызвать scribble в командной строке.
#lang scribble/base
@; Генерация PDF или HTML с помощью scribble
@(require (planet neil/numspell))
@title{99 бутылок пива}
В случае если вам не хватает @emph{бла-бла}.
@(apply itemlist
(for/list ([n (in-range 99 0 -1)])
(define N (number->english n))
(define N-- (number->english (sub1 n)))
@item{@string-titlecase[N] бутылок пива на стене,
@N бутылок пива.
Уберите одну, передайте по кругу,
@N-- бутылок пива на стене.}))
Наиболее заметная особенность Scribble — новый синтаксис, специально созданный для представления структурированного текста[4]. Этот синтаксис поддерживает свободный формат, интерполяцию строк, настраиваемые кавычки и полезен для препроцессоров, генерации текста, HTML-шаблонов. Синтаксис расширяет s-выражения, предлагая альтернативу стандартному способу ввода в языке.
#lang scribble/text
Привет,
я текстовый файл — запусти меня.
@(define (thrice . text) @list{@text, @text, @text})
@thrice{SPAM}!
@thrice{HAM}!
Typed Racket — статически типизированный диалект Racket. Его система типов уникальна тем, что разрабатывалась с целью поддержки как можно большего числа идиоматических особенностей исходного Racket, поэтому поддерживает подтипы, объединения и многое другое[5]. Важной задачей являлась также поддержка миграции частей программы в типизированный язык и обратно, реализована поддержка вызова типизированного кода из нетипизированного и наоборот, с динамической генерацией контрактов на типы[6]. Это удобно для сопровождения кода по мере развития приложения.
#lang typed/racket
;; Типизация функций высших порядков (occurrence typing)
(define-type Str-or-Num (U String Number))
(: tog ((Listof Str-or-Num) -> String))
(define (tog l)
(apply string-append (filter string? l)))
(tog (list 5 "hello " 1/2 "world" (sqrt -1)))
Язык lazy реализует семантику ленивых вычислений, аналогичную Haskell. В примере fibs — бесконечный список, при этом его 1000-й элемент вычислится только по мере необходимости.
#lang lazy
;; Бесконечный список
(define fibs
(list* 1 1 (map + fibs (cdr fibs))))
;; Вывести 1000-е число Фибоначчи
(print (list-ref fibs 1000))
В Racket входят три языка для логического программирования: Racklog (аналог Prolog), реализация Datalog и порт miniKanren. В отличие от Scribble, первые два языка используют новый для Racket синтаксис (не на базе S-выражений). В среде DrRacket подсветка, кабинеты инструментов и REPL работают аналогично поддержке Prolog/Datalog.
#lang datalog
ancestor(A, B) :- parent(A, B).
ancestor(A, B) :-
parent(A, C), D = C, ancestor(D, B).
parent(john, douglas).
parent(bob, john).
ancestor(A, B)?
Группа PLT, разрабатывающая Racket, исторически активно занимается образованием. Одним из первых экспериментов стала концепция «языковых уровней», которые ограничивают новых пользователей и обеспечивают полезные сообщения об ошибках, соответствующие уровню подготовки. Такой подход широко используется в учебниках How to Design Programs (HtDP), проекта ProgramByDesign. Далее пример работы на языке htdp/bsl («начальный уровень»). Для рисования применяется 2htdp/image, для анимации — 2htdp/universe.
#lang htdp/bsl
;; При нажатии любой клавиши шар надувается
(require 2htdp/image)
(require 2htdp/universe)
(define (balloon b) (circle b "solid" "red"))
(define (blow-up b k) (+ b 5))
(define (deflate b) (max (- b 1) 1))
(big-bang 50 (on-key blow-up) (on-tick deflate)
(to-draw balloon 200 200))
В состав Racket входит полноценная реализация языка ALGOL 60.
#lang algol60
begin
integer procedure SIGMA(x, i, n);
value n;
integer x, i, n;
begin
integer sum;
sum := 0;
for i := 1 step 1 until n do
sum := sum + x;
SIGMA := sum;
end;
integer q;
printnln(SIGMA(q*2-1, q, 7));
end
#lang plai
#lang plai-typed
Поддерживается также язык plai, который, как и racket, может быть типизированным либо нет. «Модули, написанные на plai, экспортируют все определения (в отличие от scheme)»[7]. «Язык Typed PLAI отличается от традиционного Racket в первую очередь наличием статической типизации. Также он предоставляет новые конструкции: define-type, type-case и test»[8].
Ниже приведён пример реализации нового языка:
#lang racket
(provide (except-out (all-from-out racket)
#%top #%app)
(rename-out [top #%top] [app #%app]))
(define-syntax-rule (top . x) 'x)
(define-syntax-rule (app f . xs)
(if (hash? f) (hash-ref f . xs) (f . xs)))
Этот язык:
- предоставляет все возможности языка
racket, то есть является его вариантом; - за исключением специальных «макросов-хуков», реализующих обработку необъявленных переменных и вызов функций;
- все неизвестные переменные неявно цитируются;
- хеш-таблицы могут использоваться как функции (аргументы становятся ключами для поиска).
Сохранив этот код в mylang.rkt, его можно использовать так:
#lang s-exp "mylang.rkt" ; синтаксис sexpr, семантика mylang
(define h (make-hasheq))
(hash-set! h A B) ; A и B — самоинтерпретируемые значения
(h A) ; вызов хеша как функции
Примечания
Ссылки
- racket-lang.org — официальный сайт Особенности Racket
- Документация Racket