[Перевод] Язык Janet для смертных. Часть 1 — Значения и ссылки
Это перевод небольшой книги о языке Janet за авторством Иана Генри (Ian Henry). В этой небольшой книге подробно раскрываются различные аспекты работы с языком, обьяснение синтаксиса и некоторых приемов программирования.
Внимание, в этой публикации содержатся скобочки! Люди с непереносимостью Лисп-подобных языков, вас предупредили.
Список глав:
Глава 1 — Значения и ссылки
Глава 2 — Компиляция и создание образов (исполняемых файлов)
Глава 3 — Макросы и метапрограммирование
Глава 4 — PEG-выражения
Глава 5 — Параллелизм и корутины
Глава 6 — Управление ходом исполнения программы
Глава 7 — Модули и пакеты
Глава 8 — Таблицы (ассоциативные массивы) и Полиморфизм
Глава 9 — Ксенофункции
Глава 10 — Встраивание Janet
Глава 11 — Тестирование и отладка
Глава 12 — Скриптинг
Глава 13 — Забавы с макросами
О языке Janet
На официальном сайте о языке пишут следующее:
Janet — функциональный и императивный язык программирования. Он работает на Windows, Linux, macOS, BSD и должен работать на других системах после портирования. Весь язык (основная библиотека, интерпретатор, компилятор, ассемблер, PEG) занимает менее 1 МБ. Вы также можете добавить в приложение скрипты Janet, встроив в вашу программу всего один исходный файл C и один заголовочный файл.
Дополнительно нужно сказать, что Janet сильно вдохновлена Clojure в части синтаксиса, поэтому в общем случае будет сильно походить на него, хотя и не работает на JVM. Janet динамический язык транслируемый в C. Поддерживается сборка исполняемый файлов для различных платформ и REPL.
Для установки Janet достаточно скачать всего один исполняемый файл из официального репозитория Janet-lang (если вы используете менеджер пакетов в системе, то, возможно, сможете установить Janet сразу из него).
После установки интерпретатор Janet доступен по команде janet
. Выйти из REPL можно по команде (quit)
.
Значения и ссылки
Хорошо, давайте сразу покончим со всеми условностями.
(print "Hello World")
В Janet есть скобки. Да. Это все, что стоит обсудить по этому вопросу. Возможно, скобок больше, чем вы привыкли. Возможно, больше чем вам было бы удобно. Я не собираюсь убеждать вас, что круглые скобки чем-то лучше, чем фигурные, или тратить ваше время на тезис: «это тоже самое количество скобок, просто они немного сдвинуты/написаны явно». На самом деле, я попробую как можно меньше акцентировать внимание на скобках, потому что пока они для нас не очень интересны. Они станут интересными, как только мы начнем говорить о макросах, но сейчас вы должны пройти фазу: «Фу, они мне не нравятся». Я знаю, что не нравятся. И если вы не можете пройти через это, это нормально.
На самом деле не хотелось бы вообще поднимать разговор о скобках, но было бы странно, если бы я вообще о них не упомянул. Мы же все о них думаем, верно? И теперь вам просто интересно, когда же я скажу слово на букву «Л». Вы ждете пока я его произнесу, чтобы тут же написать длинный комментарий или статью о том, что «Janet не настоящий Лисп!». Но этого не будет. Я не буду использовать слово на букву «Л» до 13 главы. К этому моменту вы уже забудете про «Л», обсуждая содержимое предыдущих глав.
Так, о чем мы говорили? Точно, наша первая программа:
(print "Hello World")
Как бы мне не хотелось поговорить о преимуществах префиксной записи, специальных формах и многом другом, но мы уже немного отстаем от «графика» и поэтому чуть-чуть пропустим и пойдем дальше:
(defmacro each-reverse [identifier list & body]
(with-syms [$list $i]
~(let [,$list ,list]
(var ,$i (- (,length ,$list) 1))
(while (>= ,$i 0)
(def ,identifier (in ,$list ,$i))
,;body
(-- ,$i)))))
(defn rewrite-verbose-assignments [tagged-lines]
(def result @[])
(var make-verbose false)
(each-reverse line tagged-lines
(match line
[:assignment identifier contents]
(if make-verbose
(array/push result [:verbose-assignment identifier contents])
(array/push result [:assignment contents]))
(array/push result line))
(match line
[:output _] (set make-verbose true)
_ (set make-verbose false)))
(reverse! result)
result)
Вот, отлично! Я думаю, что это вполне адекватный пример кода, который должен следовать сразу после «Hello World».
Пока просто задумайтесь, не пытайтесь понять как работает этот код. Разберем некоторые важные особенности:
Ужасное нагромождение пунктуации в самом начале.
Да, это так. Это некрасиво: запятые в начале слов, точки с запятой в случайных местах, непонятные знаки доллара. Я не буду объяснять или защищать эту конструкцию, скажу лишь, что по моему мнению, символы выбраны довольно удачно, если знать их назначение.
Это не тот код, который вы будете часто писать. Это код, который вы будете писать и видеть иногда, поэтому я не хочу ограждать вас от макросов, будто бы это какая-то сверх-интеллектуальная конструкция доступная только высоколобым знатокам и которую обычные люди не в состоянии понять. Они уродливые, но они работают.Круглых скобок много, квадратных скобок тоже не меньше.
Синтаксис Janet во многом был вдохновлен Clojure, в котором используются не только квадратные, но и фигурные скобки.
Указываются два разных способа создания локальных переменных
def
иvar
var
/../def
иvar
аналогичныconst
иlet
из JavaScript.var
вводит «переменную», которую можно изменить, аdef
«связывает» значение с именем, изменить которое… нельзя.array/push
выглядит как какое-то пространство имен.В JavaScript
(array/push list "item")
будет простоlist.push("item")
,
а «пространство имен» будет прототипом объекта, просматриваемым во
время выполнения.
Janet поддерживает аналогичный стиль объектно-ориентированного программирования, но он используется гораздо реже, чем в JavaScript — обычно только тогда, когда вам нужен полиморфизм во время исполнения. Вместо этого, связанные функции группируются в модули и импортируются в код. Для такого существует специальное соглашениеимя модуля/имя функции
Мы определили собственную конструкцию языка.
В Janet вы можете использовать цикл for-each, например
(each item list (print item))
. В примере мы определили свою конструкциюeach-reverse
, которая будет работать также, но будет итерироваться от конца к началу списка.
В JavaScript вы обычно создаете новое поведение при помощи передачи функций первого порядка, например,list.forEachReverse(x => ...)
. В этом нет ничего плохого, и вы можете написать то же самое на Janet, но тот факт, что вы можете определять новые структуры управления, — это одна из вещей, которая отличает Janet от большинства других языков сценариев, поэтому я хотел продемонстрировать это здесь.Мы определили функцию без явного возвращаемого значения.
Janet — это «ориентированный на выражения» язык, такой как Ruby, Haskell или Rust, а не «ориентированный на операции» язык, такой как JavaScript, C или Python.
Можно еще долго продолжать разбирать этот пример. Например, попытаться понять, что вообще делает этот код, но будет сложно рассказывать о продвинутых вещах, если мы еще не обсудили основы.
И это, наконец, возвращает нас к теме главы. Значения, существительные Janet, вещи, лежащие в основе программ на Janet — примитивные типы данных и встроенные коллекции, которые мы будем использовать для создания наших программ. Как только мы поговорим о них, остаток книги можно будет посвятить рассказу о глаголах Janet.
Итак, например (прим. переводчика: repl>> означает приглашение ко вводу в REPL Janet):
repl>> 123
123
repl>> 1e6
1000000
repl>> 1_000
1000
repl>> -0x10
-16
repl>> 10.5
10.5
Как и в JavaScript, все числа в Janet представляют собой 64-битные числа с плавающей точкой двойной точности IEEE-754. У Джанет нет «числовой башни».
repl>> true
true
repl>> false
false
repl>> maybe
just kidding
Как и в JavaScript, в Janet есть понятие «ложности». Но хотя правила вычисления, того является значение ложным или нет, в JavaScript являются распространенным источником ошибок, правила Janet гораздо проще: false
и nil
являются ложными; все остальное правда.
repl>>(truthy? 0)
true
repl>>(truthy? [])
true
repl>>(truthy? "")
true
repl>>(truthy? nil)
false
repl>>(truthy? false)
false
repl>>nil
nil
Значение nil
это эквивалент неопределенного значения (undefined
) из JavaScript. Это то, что возвращают функции, если ничего явно не вернули. Это то, что вернется вам, если вы ищете несуществующее значение.
В Janet нет эквивалента null
— не существует специального
значения типа object
, которое на самом деле не является объектом в
каком-либо значимом смысле этого слова. nil
как и undefined
это отдельные типы.
Обратите внимание, что пустой список в Janet не эквивалентен nil
. Если вы не понимаете, зачем я это проговариваю, можете просто пропустить этот абзац.
repl>>"hello"
"hello"
repl>>`"backticks"`
"\"backticks\""
repl>>``"many`backticks"``
"\"many`backticks\""
Строки бывают двух видов:
Изменяемые. Изменяемые строки называются «буфер» и начинаются с
@
Неизменяемые. Называются «строками», начинаются с
"
или`
repl>>@"this is a buffer"
@"this is a buffer"
Строки Janet представляют собой простые массивы байтов. Они не поддерживают кодировки, и в языке нет встроенной поддержки Unicode для индексации или перебора «символов». Существуют некоторые функции, которые интерпретируют строки и буферы как символы в кодировке ASCII, они называются string/ascii-upper
и string/ascii-lower
.
Существуют внешние библиотеки для декодирования UTF-8, но их нет для других известных мне кодировок. И насколько мне известно, в Janet нет полноценной библиотеки Unicode — если вам нужно подсчитать количество расширенных кластеров графем в строке, вам придется написать это самостоятельно.
repl>>[1 "two" 3]
(1 "two" 3)
repl>>["one" [2] "three"]
("one" (2) "three")
repl>>@[1 "two" 3]
@[1 "two" 3]
Векторы бывают двух видов:
Изменяемые. Изменяемые векторы называются «массивами» и начинаются с
@
Неизменяемые, называются «кортежами». Если вы привыкли к кортежам в других языках, не обманывайтесь: кортежи Janet ведут себя не так, как кортежи в любом другом языке. Это итерируемые неизменяемые векторы с произвольным доступом.
Также стоит отметить, что кортежи Janet не являются странными неизменяемыми векторами, как в Clojure. Если вы хотите добавить что-то в кортеж, вам придется сначала создать его совершенно новую копию. Чуть позже мы поговорим подробнее о различиях между изменяемыми и неизменяемыми значениями.
repl>>{:hello "world"}
{:hello "world"}
repl>>@{"hello" "world" :x 1 :a 2}
@{"hello" "world" :a 2 :x 1}
И снова изменяемые и неизменяемые типы данных.
Изменяемые таблицы называются «таблицами» и начинаются с
@
.Неизменяемые таблицы называются «структурами». Изменение структуры, как и изменение кортежа, требует сначала создания поверхностной копии.
Таблицы во многом похожи на объекты JavaScript, за исключением того, что ключи не обязательно должны быть строками, вновь созданные таблицы и структуры не имеют стандартного прототипа «корневого класса» и не могут хранить nil
ни в качестве ключей, ни в качестве значений.
Это логично, если думать о nil
как о неопределенном значении: нет никакой двусмысленности между «ключ не существует» и «ключ существует, но его значение неопределенно». Подробнее об этом мы поговорим в восьмой главе.
repl>>:hello
:hello
repl>>(keyword "world")
:world
Обычно вы используете ключевые слова в качестве ключей или имен полей в
структурах и таблицах. Они также удобны, когда вам нужно передать
неизменяемый именованный литерал, например тег или перечисление.
В JavaScript на самом деле нет аналога ключевых слов, хотя вы, возможно,
знакомы с идеей Ruby, где они называются «символами». В JavaScript вы
просто передаете небольшие строки, что функционально одно и то же.
Разница в Janet заключается в том, что ключевые слова интернируются, а
строки нет.
repl>>'hello
hello
repl>>(symbol "hello")
hello
Символы, строго говоря, точно такие же, как ключевые слова. Они используют одну и ту же таблицу для интернирования; единственная разница между ключевым словом и символом — это их тип.
Однако логически символы не представляют собой небольшие константные строки, означающие перечисления. Символы представляют собой идентификаторы в вашей программе. Вы будете часто использовать символы при написании макросов и больше нигде. Я имею в виду, что вы могли бы использовать их в другом месте, если бы вам действительно этого хотелось, но обычно удобнее использовать ключевые слова.
repl>>(fn [x] (+ x 1))
Функции Janet могут иметь переменное количество аргументов и поддерживают необязательные и именованные аргументы. fn
создает анонимную функцию, но вы также можете использовать defn
как сокращение для (def name (fn ...))
.
repl>>(defn sum [& args] (+ ;args))
repl>>(sum 1 2 3)
6
&
в списке параметров делает функцию принимающей переменное количество аргументов, а (+ ;args)
— это способ обращения к пришедшим аргументам (;
похоже на ...
в JavaScript) Как видите, функция +
уже является вариативной, поэтому нет реальной причины писать такую функцию. Но это всего лишь пример.
repl>>(defn incr [x &opt n] (default n 1) (+ x n))
repl>>(incr 10)
11
repl>>(incr 10 5)
15
&opt
делает все следующие аргументы необязательными, а &named
обьявляет именованные параметры:
repl>>(defn incr [x &named by] (+ x by))
repl>>(incr 10 :by 5)
15
Однако обратите внимание, что когда мы вызываем функцию, именованные аргументы должны идти после любых позиционных аргументов.
repl>>(incr :by 5 10)
error: could not find method :+ for :by, or :r+ for nil
in incr [repl] on line 10, column 26
in _thunk [repl] (tailcall) on line 12, column 1
Потому что :by
, является допустимым аргументом для передачи позиционно.
repl>>(fiber/new (fn [] (yield 0)))
Файберы — это мощные примитивы потока управления, и им сложно дать краткое определение. Janet использует файберы для реализации обработки исключений, генераторов, динамических переменных, раннего возврата, параллелизма в стиле async/await и сопрограмм (прим. переводчика: не уверен, какой конкретно термин можно было бы использовать, как перевод. В других статьях так и пишут — файбер).
Одна очень неполная, но, возможно, полезная мысль заключается в том, что файбер — это функция, которую можно приостановить и возобновить исполнение позже. Вот только это не функция; на самом деле это полный стек вызовов. И не всегда его можно возобновить: можно и полностью остановить. Это может сбивать с толку. Знаете что? Позже мы посвятим целую главу разговору о файберах (глава 5). Возможно, мне не стоит пытаться объяснить их до этого момента.
Отлично. Кажется мы разобрали все типы значений в Janet. По крайней мере, все, о которых я знаю.
Мне очень приятно наблюдать «пейзаж» Janet с высоты птичьего полета, но по-настоящему утешает меня только то, что я могу видеть «всю дорогу до береговой линии». И пока все, что я сделал, это перечислил кучу типов. Я получил их все? Половину из них? Или я только что прикоснулся к поверхности целого океана видов и типов?
Что ж, приятной особенностью Janet является то, что она, в сущности, представляет собой пару файлов .h и .c, поэтому очень легко рассмотреть исходный код и все проверить. Итак, давайте сделаем это.
typedef enum JanetType {
JANET_NUMBER, // [x]
JANET_NIL, // [x]
JANET_BOOLEAN, // [x]
JANET_FIBER, // [x]
JANET_STRING, // [x]
JANET_SYMBOL, // [x]
JANET_KEYWORD, // [x]
JANET_ARRAY, // [x]
JANET_TUPLE, // [x]
JANET_TABLE, // [x]
JANET_STRUCT, // [x]
JANET_BUFFER, // [x]
JANET_FUNCTION, // [x]
JANET_CFUNCTION, // [ ]
JANET_ABSTRACT, // [ ]
JANET_POINTER // [ ]
} JanetType;
Хорошо, практически все типы значений мы уже рассмотрели. JANET_CFUNCTION
— это, по сути, деталь реализации; cfunction
выглядит и действует как обычная функция почти во всем, за исключением того, что она реализована на C, а не на Janet.
repl>>(type pos?)
:function
repl>>(type int?)
:cfunction
Подбронее о нативных функциях мы поговорим в главе 9.
JANET_POINTER
полезен для взаимодействия с программами на языке C. На самом деле мы не собираемся говорить об этом, но это именно то, о чем вы можете задуматься, если будете писать на Janet. Однако JANET_ABSTRACT
очень важен, поэтому нам, вероятно, следует поговорить о нем сейчас.
Тип JANET_ABSTRACT
— это тип, реализованный в коде C, с которым можно взаимодействовать, как и с любым другим значением Janet. В девятой главе мы научимся писать свои собственные, и вы получите возможность увидеть, насколько они гибкие: вы можете реализовать что угодно как абстрактный тип, и на самом деле стандартная библиотека Janet делает именно это.
Это означает, что в стандартной библиотеке Janet имеется немного больше типов, чем предполагает перечисление JanetType
, и для полноты картины я перечислю их здесь:
core/rng
(функции генерации псевдослучайных чисел)core/socket-address
core/process
core/parser
(парсер, который Janet использует для анализа кода)core/peg
(функции взаимодейтсвия с парсером грамматик)core/stream
иcore/channel
(примитивы параллельного взаимодействия)core/lock
иcore/rwlock
(многопоточные абстракции)core/ffi-signature
,core/ffi-struct
, иcore/ffi-native
, которые являются частями нового экспериментального модуля FFI, о котором в этой книге не будет говориться.core/s64
иcore/u64
(обертки для 64-битных целочисленных значений)
И это все типы, которые определены по-умолчанию в Janet.
Я имею в виду одно определение «типа». В стандартной библиотеке есть несколько экземпляров «структуры с определенной документированной формой», и вы можете вызывать эти отдельные типы, если хотите. Но теперь вы видели все типы, существующие на базовом, механическом уровне. Вы видели все строительные блоки; все остальное — просто комбинация этих блоков.
В будущих главах мы поговорим подробнее о том, как работают эти типы и что мы можем с ними делать. Но есть одна вещь, которая настолько важна и настолько примитивна, что мы собираемся поговорить о ней прямо сейчас: равенство.
В Janet, в отличие от некоторых языков, нет отдельных функций eq
и eql
, а также функций equal
и equalp
. Также нет ==
и ===
и Object.is
. У Janet есть одно реальное понятие равенства: =
repl>>(= (+ 1 1) 2)
true
Но =
означает что-то разное в зависимости от того, спрашиваете ли вы об изменяемом значении, таком как таблица или массив, или о неизменяемом значении, таком как число, ключевое слово или кортеж.
Тип | Неизменяемый | Изменяемый |
атом | число, ключевое слово, символ, nil, логическое значение | |
замыкание | функция | |
корутина | файбер | |
массив байтов | строка | буффер |
список со случайным доступом | кортеж | массив |
хэш-таблица | структура | таблица |
Изменяемые значения равны только самим себе; можно сказать, что у них есть «эталонная семантика»:
repl>>(= @[1 2 3] @[1 2 3])
false
repl>>(def x @[1 2 3])
@[1 2 3]
repl>>(= x x)
true
Тогда как неизменяемые значения имеют «семантику значений»:
repl>>(= [1 2 3] [1 2 3])
true
Это означает, что вы можете использовать неизменяемые значения в качестве ключей таблиц или структур, не беспокоясь о конкретном экземпляре, дескриптор которого у вас есть:
repl>>(def corners {[0 0] :bottom-left [1 1] :top-right})
{(0 0) :bottom-left (1 1) :top-right}
repl>>(get corners [1 1])
:top-right
Изменяемые ключи должны иметь точно идентичное значение:
repl>>(def zero-zero @[0 0])
@[0 0]
repl>>(def corners {zero-zero :bottom-left @[1 1] :top-right})
{@[1 1] :top-right @[0 0] :bottom-left}
repl>>(get corners @[0 0])
nil
repl>>(get corners zero-zero)
:bottom-left
В Janet также есть функция deep=
, которая выполняет проверку «структурного равенства» для ссылочных типов, а также функция сompare=
, которая может вызывать собственный метод сравнения значений. Но это не «настоящие» функции равенства в том смысле, что встроенные в Janet ассоциативные структуры данных — структуры и таблицы — всегда используют только =
равенство.
Но вы можете использовать deep=
для сравнения двух изменяемых значений в вашем собственном коде:
repl>>(deep= @[1 @"two" @{:three 3}] @[1 @"two" @{:three 3}])
true
Хотя стоит отметить, что значения разных типов никогда не бывают структурно равны друг другу, даже если их элементы идентичны:
repl>>(= [1 2 3] @[1 2 3])
false
repl>>(deep= [1 2 3] @[1 2 3])
false
Абстрактные типы могут быть любыми: абстрактный тип просто означает «реализованный в коде C», и в коде C можно реализовать абстрактные типы в стиле значения или в стиле ссылки. О том, как это сделать, мы поговорим в девятой главе.
Наконец, я думаю, стоит сказать еще раз: неизменные типы Janet — это простые неизменяемые типы. Это не какие-то причудливые неизменяемые значения, которые можно найти в таком языке, как Clojure. Здесь нет структурного разделения; если вы хотите добавить элемент в неизменяемый кортеж, вам необходимо сначала сделать полную копию.
Это не означает, что вы не должны добавлять что-либо в кортежи! Но это означает, что вы должны осознавать компромисс и, вероятно, предпочитать изменяемые структуры, если работаете с большими объемами данных.
Однако внутри неизменяемые типы по-прежнему передаются по ссылке. Когда вы возвращаете неизменяемую структуру из функции, вы фактически возвращаете указатель на неизменяемую структуру — вам не нужно делать их копии, чтобы передавать их «в стеке».