Особенности 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). По указанной директории производится поиск файлов с данным суффиксом и выводятся строки, удовлетворяющие паттерну.

Менеджмент ресурсов и изоляция (sandboxing)

В 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.

Программирование GUI

В 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].

Презентации (Slideshow)

Слайды презентаций могут создаваться в 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 поставляется с набором языков, некоторые из которых весьма существенно отличаются от основной версии.

Scribble

Система документации 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

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 Racket

Язык 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))

ALGOL

В состав 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

Plai и plai-typed

 #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)                    ; вызов хеша как функции

Примечания

Ссылки

Категории