Главная мысль урока: синтаксис 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, почти всё покажется знакомым. Различия в деталях:
| Clojure | Janet | JavaScript | |
|---|---|---|---|
| Ложные значения | nil, false | nil, false | false, 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 / 2 | 7/2 на JVM, 3.5 в браузере | 3.5 | 3.5 |
| Квадратные скобки | вектор, неизменяемый | кортеж, неизменяемый | массив, изменяемый |
Попробуй руками
В задаче четыре функции на материале урока: формула урона, проверка истинности, определение вида значения через cond и подпись героя через str.
Проверь себя
Задание
Дальше
В следующем уроке разберём четыре коллекции, которые ты уже видел краем глаза: список, вектор, мапу и множество, и чем они отличаются по стоимости операций.
домашка