Это перевод небольшой книги о языке 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".

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

  1. Ужасное нагромождение пунктуации в самом начале.

    Да, это так. Это некрасиво: запятые в начале слов, точки с запятой в случайных местах, непонятные знаки доллара. Я не буду объяснять или защищать эту конструкцию, скажу лишь, что по моему мнению, символы выбраны довольно удачно, если знать их назначение.
    Это не тот код, который вы будете часто писать. Это код, который вы будете писать и видеть иногда, поэтому я не хочу ограждать вас от макросов, будто бы это какая-то сверх-интеллектуальная конструкция доступная только высоколобым знатокам и которую обычные люди не в состоянии понять. Они уродливые, но они работают.

  2. Круглых скобок много, квадратных скобок тоже не меньше.

    Синтаксис Janet во многом был вдохновлен Clojure, в котором используются не только квадратные, но и фигурные скобки.

  3. Указываются два разных способа создания локальных переменных def и var
    var
    /../


    def и var аналогичны const и let из JavaScript. var вводит "переменную", которую можно изменить, а def "связывает" значение с именем, изменить которое... нельзя.

  4. array/push выглядит как какое-то пространство имен.

    В JavaScript (array/push list "item")будет просто list.push("item"),
    а "пространство имен" будет прототипом объекта, просматриваемым во
    время выполнения.
    Janet поддерживает аналогичный стиль объектно-ориентированного программирования, но он используется гораздо реже, чем в JavaScript
    — обычно только тогда, когда вам нужен полиморфизм во время исполнения. Вместо этого, связанные функции группируются в модули и импортируются в код. Для такого существует специальное соглашение имя модуля/имя функции

  5. Мы определили собственную конструкцию языка.

    В Janet вы можете использовать цикл for-each, например (each item list (print item)). В примере мы определили свою конструкцию each-reverse, которая будет работать также, но будет итерироваться от конца к началу списка.
    В JavaScript вы обычно создаете новое поведение при помощи передачи функций первого порядка, например,
    list.forEachReverse(x => ...). В этом нет ничего плохого, и вы можете написать то же самое на Janet, но тот факт, что вы можете определять новые структуры управления, — это одна из вещей, которая отличает Janet от большинства других языков сценариев, поэтому я хотел продемонстрировать это здесь.

  6. Мы определили функцию без явного возвращаемого значения.

    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))
<function 0x600000A8C9E0>

Функции Janet могут иметь переменное количество аргументов и поддерживают необязательные и именованные аргументы. fn создает анонимную функцию, но вы также можете использовать defn как сокращение для (def name (fn ...)).

repl>>(defn sum [& args] (+ ;args))
<function sum>

repl>>(sum 1 2 3)
6

& в списке параметров делает функцию принимающей переменное количество аргументов, а (+ ;args) — это способ обращения к пришедшим аргументам ( ; похоже на ... в JavaScript) Как видите, функция + уже является вариативной, поэтому нет реальной причины писать такую ​​функцию. Но это всего лишь пример.

repl>>(defn incr [x &opt n] (default n 1) (+ x n))
<function incr>

repl>>(incr 10)
11

repl>>(incr 10 5)
15

&opt делает все следующие аргументы необязательными, а &named обьявляет именованные параметры:

repl>>(defn incr [x &named by] (+ x by))
<function incr>

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)))
<fiber 0x600003C10150>

Файберы — это мощные примитивы потока управления, и им сложно дать краткое определение. 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. Здесь нет структурного разделения; если вы хотите добавить элемент в неизменяемый кортеж, вам необходимо сначала сделать полную копию.

Это не означает, что вы не должны добавлять что-либо в кортежи! Но это означает, что вы должны осознавать компромисс и, вероятно, предпочитать изменяемые структуры, если работаете с большими объемами данных.

Однако внутри неизменяемые типы по-прежнему передаются по ссылке. Когда вы возвращаете неизменяемую структуру из функции, вы фактически возвращаете указатель на неизменяемую структуру — вам не нужно делать их копии, чтобы передавать их «в стеке».

Комментарии (5)


  1. GospodinKolhoznik
    13.11.2023 12:16
    +2

    А зачем он нужен? Что нового он даёт, чего не может дать та же Clojure и куча других lisp-подобных языков?


    1. CorwinH
      13.11.2023 12:16
      +1

      Видимо, авторам нравится Clojure - "функциональный Java с макросами" и они захотели "функциональный Си с макросами".


    1. Ales_Nim Автор
      13.11.2023 12:16

      Добрый вечер,

      1. Давайте я приведу вам сравнение с "округлением в большую сторону" -- Janet это как компилируемый Clojure.

      1. Публикаций по Janet на Хабре еще не было, я решил, что будет полезно чтобы они были.

      2. С такой риторикой, получается, вообще языки не нужны. Максимум ассемблер, зачем что-то еще


      1. GospodinKolhoznik
        13.11.2023 12:16
        +1

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

        Поэтому и спросил, что он может дать такого, чего не дают другие языки из большой семьи lisp.


  1. Rigidus
    13.11.2023 12:16
    +4

    Ну статья написана достаточно полно и подробно - хороший обзор языка с рассмотрением важных сторон. Но: фич меньше чем в лиспе:

    • numerical tower нет,

    • поддержки Unicode на уровне языка нет,

    • куски идеологии раста с изменяемыми и неизменяемыми переменными есть, при этом отражены в синтаксисе, и ради этого использован символ @

    • лишние виды скобок (наследие кложуры) нарушают гомоиконность, интересно как там макросы с этим нарушением справляются

    • кложуровские идиомы с кортежами, таблицами и векторами (двух видов) - прибиты к синтаксису (хотя в общем случае было бы достаточно списка, в в частном ради производительности можно было бы использовать стандартные идиомы доступа, которые еще и обобщаются. Т.е. можно было бы использовать общие функции для вектора и списка и делать конкретизацию только ради тюнинга производительности или даже автоматически, опираясь на тип.

    • вектор отождествлен с массивом, при этом многомерных массивов нет, так что налицо другое толкование термина "массив" (это не те дроиды, которых вы ищете)

    • nil отделен от пустого списка и от false (что приводит к более многословной обработке особых ситуаций), но имеет право на жизнь

    • символы есть (но автор ссылается на руби, а не на лисп, из которого руби их унаследовал, это забавно), при этом они интернируются, но отсутстует пакетная механика, затененение и явное управление символами и их видимостью в пакете.

    • вариадические аргументы функции есть, взято из питона, но нет куда более мощной концепции lambda-list, при этом у автора уже начинают заканчиваться спецсимволы, уже используется даже точка с запятой, могу себе представить сколько времени ушло на отладку парсера и насколько сложным он стал..

    • Есть файберы, прибиты к языку, хотя в лиспе они могут быть реализованы как библиотеки

    • Непонятно состояние и интеропом (есть FFI?)

    • Проверка на равенство изолирует нас от важных деталей

    Видно что автор дал себе труд разобраться PLT и лиспах, думаю для него это хорошее упражнение по реализации языка. Думаю некоторые недостатки связаны с тем, что тяжелые в реализации вещи он постарался обойти, чтобы не делать скучную работу. Влияние "современных" языков к сожалению типично для подобных упражнений, т.к. из-за синдрома утенка мы можем часто видеть "кложуру с ароматом раст", но реже видим "лисп с ароматом эрланг" - до более нетривиальных идей тяжелее доходить, а поупражняться в создании языков хочется уже сразу