Раздел 28 · Clojure

Формы и литералы

junior~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Главная мысль урока: синтаксис Clojure помещается в один урок. Есть литералы (числа, строки, ключевые слова, коллекции) и списки, а список в коде означает вызов: первый элемент это функция, остальные аргументы. Скобки читаются как дерево, и вычисление идёт от листьев к корню.

В «Хрониках Пограничья» урон от удара считается так: удвоенная сила атаки минус броня противника, но не меньше единицы. На JavaScript ты бы написал Math.max(1, attack * 2 - armor). На Clojure то же самое выглядит так:

Непривычно, но правило одно на весь язык. Открывающая скобка начинает вызов, сразу за ней стоит то, что вызывают, дальше аргументы. Никаких приоритетов операторов, никаких «умножение раньше сложения»: порядок задают скобки.

Скобки как дерево

Каждая пара скобок это узел дерева. Первый элемент списка это функция, остальные это дети узла. Нарисуем урон из примера:

(max 1 (- (* attack 2) armor))

          max
         /   \
        1     -
             / \
            *   armor
           / \
      attack  2

Clojure вычисляет дерево снизу вверх. Сначала листья: attack это 6, armor это 4, числа остаются числами. Потом (* 6 2) даёт 12, (- 12 4) даёт 8, и наконец (max 1 8) даёт 8. Такая запись, где функция стоит перед аргументами, называется префиксной. У неё есть приятный побочный эффект: +, < и max принимают сколько угодно аргументов.

(< 1 5 10) читается как «числа идут по возрастанию». В JavaScript для этого понадобилось бы 1 < 5 && 5 < 10.

Любой кусок кода на Clojure называется формой. Число это форма, символ attack это форма, (max 1 8) это форма. REPL из прошлого урока читает форму, вычисляет и печатает результат.

Скаляры

Простые значения, из которых собирается всё остальное:

Ты уже знаком почти со всеми. Ключевое слово :hero/hp означает само себя, на нём держатся мапы, его подробно разберём в уроке про мапы. Символ attack это имя: при вычислении на его место встаёт значение. Кавычка перед символом запрещает это, и 'attack остаётся символом. nil означает «ничего», как null в JavaScript. #"\d+" это регулярное выражение.

Числа: тут хозяин виден

С числами браузерная песочница честно отличается от настоящего Clojure. Здесь хозяин JavaScript, и числа это его числа: целые и дробные одного типа, а деление всегда даёт дробь в десятичной записи.

На JVM (/ 7 2) даёт 7/2: это ratio, точная дробь без округления. Целые и дробные числа там разных типов, деление на ноль бросает исключение, а сложить число со строкой нельзя:

(println (/ 7 2))                ; 7/2
(println (class (/ 7 2)))        ; clojure.lang.Ratio
(println (+ 1/3 2/3))            ; 1N, точно, без 0.9999
(println (/ 7 2.0))              ; 3.5, если дробь нужна в десятичной записи
(println (class 7) (class 3.5))  ; java.lang.Long java.lang.Double
(println (try (/ 1 0) (catch ArithmeticException e (ex-message e))))  ; Divide by zero
(println (try (+ 1 "2") (catch ClassCastException _ :class-cast)))     ; :class-cast
(assert (= 7/2 (/ 7 2)))

Если пишешь код, который должен одинаково работать в обоих местах, используй quot для целочисленного деления и не полагайся на ratio. В задачах раздела так и сделано.

С символами то же самое: на JVM \a это отдельный тип, символ Java. В браузере у JavaScript такого типа нет, и \a становится строкой "a".

Истинность: ложны только двое

В if Clojure проверяет одно: не nil ли значение и не false ли оно. Всё остальное истинно, в том числе ноль, пустая строка и пустой вектор:

В JavaScript 0 и "" ложны, и это источник классического бага: «у героя ноль золота, значит, золота нет вообще». В Clojure ноль это ноль, а отсутствие значения это nil. Правило звучит коротко: ложны только nil и false.

Особые формы

Если список это вызов функции, как тогда написать if? Функция вычисляет все аргументы до вызова, а if должен вычислить только одну ветку. Поэтому в языке есть десяток особых форм. Выглядят они как вызов, но вычисляются по своим правилам.

  • def связывает имя с значением на уровне неймспейса. Это не переменная в смысле JavaScript: перевязывать её в коде не принято, только в REPL при правке.
  • if вычисляет условие и одну из двух веток.
  • do вычисляет формы по порядку и возвращает последнюю. Нужен, когда в ветке if надо сделать два дела.
  • let даёт локальные имена. Пары внутри квадратных скобок: имя и значение. Имена видны только внутри let.
  • quote и короткая запись ' отдают форму как данные, не вычисляя её.

Последний пункт стоит прочувствовать. (+ 1 2) это вызов, он даёт 3. '(+ 1 2) это список из трёх элементов: символа + и двух чисел. Код на Clojure записан структурами данных самого языка, и это свойство называется гомоиконностью. На нём держатся макросы, им посвящён урок про код как данные.

Поверх особых форм стандартная библиотека строит конструкции покороче. when это if без ветки «иначе» и со встроенным do. cond проверяет условия по очереди и возвращает значение первой истинной ветки:

Последнее условие :else это обычное ключевое слово, а оно истинно. Можно написать :otherwise или true, но :else общепринято.

Рядом с Janet и JavaScript

Если ты проходил урок про литералы в Janet, почти всё покажется знакомым. Различия в деталях:

ClojureJanetJavaScript
Ложные значенияnil, falsenil, falsefalse, 0, "", null, undefined, NaN
Вызов(f a b)(f a b)f(a, b)
Ключевое слово:hp:hpнет, строка "hp"
Локальные имена(let [a 1] ...)(let [a 1] ...)const a = 1
Деление 7 / 27/2 на JVM, 3.5 в браузере3.53.5
Квадратные скобкивектор, неизменяемыйкортеж, неизменяемыймассив, изменяемый

Попробуй руками

В задаче четыре функции на материале урока: формула урона, проверка истинности, определение вида значения через cond и подпись героя через str.

Проверь себя

Задание

Дальше

В следующем уроке разберём четыре коллекции, которые ты уже видел краем глаза: список, вектор, мапу и множество, и чем они отличаются по стоимости операций.

домашка

Домашка