Раздел 31 · Janet на практике

Литералы и коллекции: всё парами

junior~30 мин

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

Литералы и коллекции: всё парами

Весь синтаксис данных Janet за один урок. Главная идея, ради которой стоит читать внимательно: у большинства типов есть изменяемая и неизменяемая версия, и знак @ помечает изменяемую. Понимание этой симметрии снимает половину будущих вопросов.

Вот кадр, с которого всё начинается. Ты открываешь чужой код и видишь рядом [1 2 3] и @[1 2 3]. Зачем языку два способа записать один список? Затем, что это разные типы: кортеж, который никогда не изменится, и массив, который можно наращивать. Та же собака зарыта у строк с буферами и у структур с таблицами: три пары близнецов, различающиеся одним знаком @. Весь урок раскручивает эту симметрию.

Числа

Начнём с исключения: числа пары не образуют, зато прячут свои ловушки. Числовой тип в Janet один: 64-битное число с плавающей точкой (IEEE 754). Отдельного целочисленного типа нет:

Деление всегда даёт дробный результат, для целочисленного есть отдельная функция:

Ловушка: % и mod расходятся на отрицательных числах. % это остаток от деления, знак берётся от делимого. mod это математический модуль, знак берётся от делителя:

Для индекса по кругу (кольцевой буфер, обход списка с переходом на начало) почти всегда нужен mod, а не %. Смотри, что происходит при шаге назад с нулевого индекса:

Ошибка здесь тихая: пока индекс движется вперёд, оба варианта совпадают, и тесты на положительных числах проходят.

Записать число можно в другой системе счисления и с разделителями:

Строки и буферы

Здесь впервые появляется тема пар. Для текста это строка и буфер:

"строка"       # :string, неизменяемая
@"буфер"       # :buffer, изменяемый

Строки в двойных кавычках понимают экранирование (\n, \t, \", \\). Когда экранирование мешает (регулярное выражение, путь Windows, код внутри кода), берут длинные строки в обратных кавычках:

``здесь \n не спецсимвол, а два обычных знака``

Длинную строку открывают любым количеством обратных кавычек, закрывают таким же. Это позволяет вложить внутрь текст, в котором сами бэктики есть:

```строка с ``двумя бэктиками`` внутри```

Строка это байты, а не символы. Janet не знает про Unicode, строка для него последовательность байтов. Для кириллицы это значит, что length считает байты:

Хранится и выводится UTF-8 корректно, но посимвольная обработка требует внимания. Мы вернёмся к этому в уроке про PEG, где то же свойство определяет, как пишутся грамматики для кириллицы.

Ключевые слова и символы

:keyword       # :keyword, вычисляется в само себя
'symbol        # :symbol, обычно имя переменной

Ключевое слово это самоопределяемая константа. Пишешь :name, получаешь :name. Их берут как ключи словарей, метки вариантов и именованные аргументы. Аналоги: :keyword в Clojure, Symbol в Ruby, строковые enum в TypeScript.

Символ это имя. Когда Janet видит foo, он ищет значение, связанное с этим именем. Апостроф перед символом отменяет поиск и возвращает само имя как значение:

Эта разница станет центральной в уроке про макросы. Пока запомни: апостроф говорит “не вычисляй, отдай как есть”.

nil и истинность

nil            # отсутствие значения
true
false

Правило истинности отличается от большинства языков: ложны ровно два значения, nil и false. Ноль, пустая строка, пустой массив, всё это истина:

Никаких falsy-значений, к которым приучил JavaScript. Проверку на пустоту пиши явно: (empty? xs), (zero? n). Если ты пришёл из JS, где if (arr.length) это идиома, тут придётся один раз переучиться, зато потом не будет класса ошибок про случайно проглоченный ноль. Мы разбирали цену неявных приведений в уроке про логические приведения в JavaScript.

Четыре коллекции, две пары

НеизменяемаяИзменяемаяНазначение
Последовательностькортеж [1 2 3]массив @[1 2 3]список значений по индексу
Словарьструктура {:a 1}таблица @{:a 1}отображение ключ в значение

В словарях нет запятых и двоеточий-разделителей, только чередование ключей и значений. {:a 1 :b 2} читается как ключ :a даёт 1, ключ :b даёт 2.

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

Чтение элементов

(get [10 20 30] 1)      # 20
(get {:a 1} :a)         # 1
(get [10 20 30] 99)     # nil, выхода за границы нет, есть nil

Коллекции вызываемы: структуру можно поставить на место функции, и она сработает как get:

Ловушка для тех, кто пришёл из Clojure. Помнишь гипотезу, которую ты формулировал в 31-janet/01 · Зачем Janet, лисп на один мегабайт? Вот и ответ. Обратный порядок, (:a {:a 1}), в Clojure достаёт значение, а в Janet возвращает nil. Причина не в том, что ключевые слова невызываемы. Запись (:ключ объект) это вызов метода: Janet ищет ключ в объекте и вызывает найденное как функцию, передав ей сам объект. В {:a 1} под ключом :a лежит число 1, вызвать его нельзя, отсюда nil. А если ключа нет вовсе, будет уже не nil, а ошибка:

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

Отрицательные индексы и срезы:

(last [1 2 3])           # 3
(slice [1 2 3 4] 1 3)    # (2 3)
(slice "hello" 1 -2)     # "ell", -1 означает до конца

Изменение

Изменяемые коллекции меняются на месте:

Неизменяемые не меняются. put на структуре это ошибка, а не тихое создание копии. Язык не притворяется, что всё получилось.

Равенство

Это прямое следствие изменяемости, и оно того стоит: неизменяемые значения можно хешировать, поэтому кортежи и структуры годятся в ключи словарей, а массивы нет. Разберём подробно в уроке про значения и ссылки.

Обход

Обрати внимание на @ в результатах: map и filter возвращают массив, даже если на вход дали кортеж. Функции обработки коллекций в Janet почти всегда строят изменяемый результат. Если нужен неизменяемый, заворачивай в tuple или freeze явно.

Что запомнить

  • Число одно, 64-битное с плавающей точкой. div для целочисленного деления, mod вместо % для индексов по кругу.
  • Строка это байты. length на кириллице считает не буквы.
  • @ помечает изменяемый вариант: @"буфер", @[массив], @{таблица}.
  • Ложны ровно nil и false. Ноль и пустая строка истинны.
  • Кортеж и структура сравниваются по содержимому и годятся в ключи. Массив и таблица сравниваются по идентичности и не годятся.
  • Коллекция вызываема и работает как get. Обратный порядок (:ключ объект) это вызов метода, а не чтение.
  • map и filter всегда отдают массив.

Упражнения

Дальше

Синтаксис данных закрыт. Следующий урок про синтаксис кода: def и var, функции, деструктуризация, а главное, как читать скобки, чтобы они перестали быть препятствием.

домашка

Домашка