LEARN X · ЗА 15 МИН
Racket
Racket за 15 минут: весь язык на одной странице в комментариях кода — define, lambda, match, struct, list, map, hash, vector, for.
Racket — потомок Scheme и диалект Lisp с богатой экосистемой, мощным сопоставлением с образцом (match) и развитой системой макросов. Весь синтаксис — это списки в круглых скобках: (функция аргумент1 аргумент2). Ниже — весь язык в комментариях рабочего кода.
Начало: #lang, вывод и комментарии
Каждая программа начинается со строки #lang, объявляющей диалект.
#lang racket ; диалект языка: основной — racket
; Однострочный комментарий начинается с точки с запятой ;
#|
Многострочный (блочный) комментарий
заключается между #| и |#
|#
#;(это выражение целиком закомментировано датум-комментарием)
(displayln "Привет, мир!") ; печать значения + перевод строки
(display "без переноса") ; печать без перевода строки
(newline) ; явный перевод строки
(print "со скобками строка"); печать в read-форме (с кавычками)
; printf — форматированный вывод; ~a — человекочитаемо, ~v — read-форма
(printf "Сумма ~a и ~a равна ~a\n" 2 3 (+ 2 3)) ; Сумма 2 и 3 равна 5
Переменные: define, let, let*
define создаёт глобальное (или модульное) имя, let — локальное.
(define x 10) ; связываем имя x со значением 10
(define имя "Аня") ; имена могут быть на кириллице
(define pi 3.14159)
; let — локальные привязки; видны только внутри тела
(let ([a 1]
[b 2])
(+ a b)) ; => 3, a и b снаружи не видны
; let* — последующие привязки видят предыдущие
(let* ([a 5]
[b (* a 2)]) ; b может ссылаться на уже объявленное a
(+ a b)) ; => 15
; letrec — для взаимно-рекурсивных определений
(letrec ([even? (lambda (n) (if (= n 0) #t (odd? (- n 1))))]
[odd? (lambda (n) (if (= n 0) #f (even? (- n 1))))])
(even? 10)) ; => #t
; set! изменяет уже существующую привязку (мутация)
(define счётчик 0)
(set! счётчик (+ счётчик 1)) ; счётчик теперь 1
Типы: числа, строки, символы, булевы
; --- Числа: точные и неточные ---
42 ; целое (exact integer)
(/ 1 3) ; => 1/3 — точная дробь (рациональное число!)
3.14 ; неточное (inexact, число с плавающей точкой)
(exact->inexact 1/3); => 0.3333333333333333
(inexact->exact 0.5); => 1/2
1+2i ; комплексное число
(expt 2 100) ; => огромное целое (произвольная точность)
; Арифметика — префиксная: оператор идёт первым
(+ 1 2 3) ; => 6 (любое число аргументов)
(- 10 3 2) ; => 5
(* 2 3 4) ; => 24
(quotient 17 5) ; => 3 (целочисленное деление)
(remainder 17 5) ; => 2 (остаток)
(modulo -7 3) ; => 2
; --- Строки ---
"строка в двойных кавычках"
(string-append "Привет, " "Racket") ; => "Привет, Racket"
(string-length "абвг") ; => 4
(substring "Racket" 0 3) ; => "Rac"
(string-upcase "racket") ; => "RACKET"
(string->number "42") ; => 42
(number->string 42) ; => "42"
(format "~a + ~a" 2 3) ; => "2 + 3"
; --- Символы (symbols): легковесные интернированные имена ---
'привет ; символ привет (с апострофом — не вычисляется)
(symbol->string 'abc); => "abc"
(eq? 'a 'a) ; => #t (символы сравниваются мгновенно)
; --- Булевы значения ---
#t ; истина (можно писать #true)
#f ; ложь (можно писать #false)
; ВАЖНО: ложно ТОЛЬКО #f, всё остальное (0, "", '()) — истинно!
(and #t #t #f) ; => #f
(or #f #f #t) ; => #t
(not #f) ; => #t
Функции: define, lambda, аргументы
; Определение функции — короткая форма
(define (квадрат x)
(* x x))
(квадрат 5) ; => 25
; lambda — анонимная функция
(define куб (lambda (x) (* x x x)))
(куб 3) ; => 27
((lambda (x) (+ x 1)) 41) ; => 42 (вызов на месте)
; Переменное число аргументов: . собирает остаток в список
(define (сумма-всех . числа)
(apply + числа))
(сумма-всех 1 2 3 4) ; => 10
; Аргументы по умолчанию (необязательные)
(define (приветствие имя [восклицание "!"])
(string-append "Привет, " имя восклицание))
(приветствие "Аня") ; => "Привет, Аня!"
(приветствие "Аня" "?") ; => "Привет, Аня?"
; Именованные (keyword) аргументы — через #:
(define (точка #:x x #:y y)
(list x y))
(точка #:y 2 #:x 1) ; => '(1 2) (порядок не важен)
; Каррирование: функция, возвращающая функцию
(define ((прибавить n) m) (+ n m))
(define прибавить5 (прибавить 5))
(прибавить5 10) ; => 15
((прибавить 3) 4) ; => 7
; curry из стандартной библиотеки
(define inc (curry + 1))
(inc 9) ; => 10
Условия: if, cond, when, unless, case
; if — ровно две ветви: (if УСЛОВИЕ ТО ИНАЧЕ)
(if (> 5 3) "больше" "меньше") ; => "больше"
; cond — цепочка условий, else в конце
(define (знак n)
(cond
[(> n 0) "положительное"]
[(< n 0) "отрицательное"]
[else "ноль"]))
(знак -7) ; => "отрицательное"
; cond с => передаёт результат теста в функцию
(cond
[(member 2 '(1 2 3)) => (lambda (rest) (car rest))] ; => 2
[else 'нет])
; when — выполнить тело, ЕСЛИ условие истинно (без ветви else)
(when (> 10 5)
(displayln "десять больше пяти"))
; unless — выполнить, ЕСЛИ условие ЛОЖНО
(unless (= 1 2)
(displayln "один не равен двум"))
; case — сравнение значения по веткам (через equal?)
(define (день-недели n)
(case n
[(1 2 3 4 5) "будни"]
[(6 7) "выходные"]
[else "нет такого дня"]))
(день-недели 6) ; => "выходные"
Списки: cons, list, first/rest, кавычка
Список — основа Lisp: цепочка пар (cons-ячеек), заканчивающаяся пустым списком '().
'(1 2 3) ; литерал списка (апостроф = quote, не вычислять)
(list 1 2 3) ; то же самое, но аргументы вычисляются
(list (+ 1 1) (* 2 2)) ; => '(2 4)
'() ; пустой список
empty ; тоже пустой список
; cons добавляет элемент в начало (строит пару)
(cons 0 '(1 2 3)) ; => '(0 1 2 3)
(cons 1 2) ; => '(1 . 2) — точечная пара (не список)
; Доступ к элементам
(first '(1 2 3)) ; => 1 (синоним car)
(rest '(1 2 3)) ; => '(2 3) (синоним cdr)
(second '(1 2 3)) ; => 2
(last '(1 2 3)) ; => 3
(list-ref '(a b c) 1) ; => 'b (по индексу)
(length '(1 2 3)) ; => 3
; Операции со списками
(append '(1 2) '(3 4)) ; => '(1 2 3 4)
(reverse '(1 2 3)) ; => '(3 2 1)
(member 2 '(1 2 3)) ; => '(2 3) (хвост от найденного, или #f)
(range 0 5) ; => '(0 1 2 3 4)
(list->string '(#\h #\i)); => "hi"
(empty? '()) ; => #t
(null? '()) ; => #t
Функции высшего порядка: map, filter, foldl/foldr, for/list
; map — применить функцию к каждому элементу
(map (lambda (x) (* x x)) '(1 2 3 4)) ; => '(1 4 9 16)
(map + '(1 2 3) '(10 20 30)) ; => '(11 22 33) (несколько списков)
; filter — оставить элементы, проходящие предикат
(filter even? '(1 2 3 4 5 6)) ; => '(2 4 6)
(filter (lambda (x) (> x 2)) '(1 2 3 4)) ; => '(3 4)
; foldl / foldr — свёртка списка в одно значение
; (foldl ФУНКЦИЯ НАЧАЛО СПИСОК); аккумулятор идёт ВТОРЫМ аргументом
(foldl + 0 '(1 2 3 4)) ; => 10
(foldl cons '() '(1 2 3)) ; => '(3 2 1) (foldl разворачивает)
(foldr cons '() '(1 2 3)) ; => '(1 2 3) (foldr сохраняет порядок)
; apply — раскрыть список в аргументы функции
(apply max '(3 1 4 1 5 9)) ; => 9
; for/list — генератор списка через итерацию
(for/list ([x (range 1 6)])
(* x x)) ; => '(1 4 9 16 25)
; with условием (#:when)
(for/list ([x (range 1 11)] #:when (even? x))
x) ; => '(2 4 6 8 10)
; Композиция функций
((compose1 add1 (lambda (x) (* x 2))) 5) ; => 11 (5*2, потом +1)
Сопоставление с образцом: match
match — одна из самых мощных фишек Racket: разбор структуры данных по образцам.
; Базовое сопоставление по значениям
(define (описать x)
(match x
[0 "ноль"]
[1 "один"]
[_ "что-то другое"])) ; _ — образец-джокер (любое значение)
(описать 1) ; => "один"
; Разбор списков: связываем переменные из структуры
(match '(1 2 3)
[(list a b c) (+ a b c)]) ; => 6 (a=1, b=2, c=3)
; Хвост списка через многоточие ...
(match '(1 2 3 4 5)
[(list first rest ...) rest]) ; => '(2 3 4 5)
; cons-образец: голова и хвост
(match '(10 20 30)
[(cons head tail) (list head tail)]) ; => '(10 (20 30))
; Сторожевые условия (#:when) и литералы
(define (классифицировать n)
(match n
[(? negative?) "отрицательное"]
[0 "ноль"]
[(? even?) "чётное положительное"]
[_ "нечётное положительное"]))
(классифицировать 4) ; => "чётное положительное"
; Сопоставление со структурами и захват целого через образец
(match (list 1 2)
[(list (and whole all) ...) whole]) ; => '(1 2)
Структуры: struct
; struct определяет новый составной тип
(struct точка (x y) #:transparent)
; #:transparent — чтобы значение красиво печаталось и сравнивалось equal?
(define p (точка 3 4)) ; конструктор: имя структуры
(точка-x p) ; => 3 (автоматический аксессор имя-поле)
(точка-y p) ; => 4
(точка? p) ; => #t (предикат имя?)
; Структуры можно разбирать через match
(match p
[(точка x y) (sqrt (+ (* x x) (* y y)))]) ; => 5 (длина вектора)
; Наследование структур
(struct цветная-точка точка (цвет) #:transparent)
(define cp (цветная-точка 1 2 'красный))
(точка-x cp) ; => 1 (унаследованное поле)
(цветная-точка-цвет cp) ; => 'красный
; Изменяемые поля через #:mutable
(struct ячейка (значение) #:mutable)
(define c (ячейка 10))
(set-ячейка-значение! c 99)
(ячейка-значение c) ; => 99
Хеши и векторы
; --- Хеш-таблицы (словари) ---
(define h (hash 'a 1 'b 2 'c 3)) ; неизменяемый хеш
(hash-ref h 'a) ; => 1
(hash-ref h 'z 0) ; => 0 (значение по умолчанию)
(hash-set h 'd 4) ; => новый хеш с добавленной парой
(hash-keys h) ; => '(a b c) (порядок не гарантирован)
(hash-values h) ; => '(1 2 3)
(hash-has-key? h 'b) ; => #t
(hash-count h) ; => 3
; Изменяемый хеш
(define mh (make-hash))
(hash-set! mh 'ключ 42)
(hash-ref mh 'ключ) ; => 42
; Перебор хеша
(for ([(k v) (in-hash h)])
(printf "~a => ~a\n" k v))
; --- Векторы: массивы с доступом по индексу за O(1) ---
(define v (vector 10 20 30))
(vector-ref v 0) ; => 10
(vector-length v) ; => 3
(define mv (make-vector 5 0)) ; вектор из 5 нулей
(vector-set! mv 2 99) ; изменяем элемент по индексу
(vector-ref mv 2) ; => 99
(vector->list v) ; => '(10 20 30)
(list->vector '(1 2 3)) ; => #(1 2 3)
Итерации: for, for/list, for/sum
; for — цикл с побочными эффектами (ничего не возвращает полезного)
(for ([i (range 3)])
(displayln i)) ; печатает 0, 1, 2
; Несколько последовательностей идут параллельно
(for ([x '(a b c)]
[i (range 3)])
(printf "~a: ~a\n" i x)) ; 0: a / 1: b / 2: c
; for/list — собрать результаты в список
(for/list ([i (range 5)]) (* i 10)) ; => '(0 10 20 30 40)
; for/sum — сумма всех значений тела
(for/sum ([i (range 1 101)]) i) ; => 5050
; for/product — произведение
(for/product ([i (range 1 6)]) i) ; => 120 (факториал 5)
; for/and, for/or — логическая свёртка
(for/and ([x '(2 4 6)]) (even? x)) ; => #t
; for/hash — построить хеш в цикле
(for/hash ([i (range 3)])
(values i (* i i))) ; => хеш {0:0, 1:1, 2:4}
; Вложенные итерации: for* (декартово произведение)
(for*/list ([x '(1 2)] [y '(a b)])
(list x y)) ; => '((1 a) (1 b) (2 a) (2 b))
Особенности: контракты и типизированный Racket
Racket — это «язык для создания языков». Помимо проверок во время выполнения, есть отдельный диалект со статической типизацией.
; --- Контракты: проверки на границе модуля ---
(require racket/contract)
; provide/contract гарантирует тип/условие при экспорте
(define (удвоить n) (* 2 n))
(provide
(contract-out
[удвоить (-> number? number?)])) ; принимает число — возвращает число
; Контракт можно навесить локально
(define/contract (корень x)
(-> (and/c real? (not/c negative?)) real?) ; вход: неотрицательное вещественное
(sqrt x))
(корень 16) ; => 4
; (корень -1) — нарушение контракта, понятная ошибка
; --- Типизированный Racket (отдельный #lang) ---
; В отдельном файле первой строкой пишут:
; #lang typed/racket
; и аннотируют типы:
;
; (: площадь (-> Real Real Real))
; (define (площадь ширина высота)
; (* ширина высота))
;
; Типы проверяются статически, ДО запуска программы.
; --- Бонус: макросы делают язык расширяемым ---
(define-syntax-rule (свой-if усл то иначе)
(cond [усл то] [else иначе]))
(свой-if (> 2 1) 'да 'нет) ; => 'да — мы добавили новую форму в язык!