Литералы и коллекции: всё парами
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Литералы и коллекции: всё парами
Весь синтаксис данных 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, функции, деструктуризация, а главное, как читать скобки, чтобы они перестали быть препятствием.
домашка