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

Модули: файл, таблица, окружение

middle~35 мин

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

Модули: файл, таблица, окружение

В Janet нет объявления модуля. Любой файл уже модуль, а после загрузки он превращается в обычную таблицу связываний. Из этих двух фактов выводится всё остальное поведение системы модулей.

Модуль это файл

Пусть есть lib/calc.janet:

(defn double-it [x] (* 2 x))
(def the-const 42)

Подключаем:

(import ./lib/calc :as c)

(c/double-it 21)   # 42
c/the-const        # 42

Путь, начинающийся с ./, отсчитывается от файла, в котором написан import, а не от текущего каталога. Это важно: модуль остаётся рабочим, откуда бы ни запустили программу. Голые имена, без ./, ищутся по другим правилам; разберём их в конце урока.

Пять форм импорта

Пощёлкай формы и посмотри, что оказывается в пространстве имён:

:as это короткий псевдоним и основная форма, бери её по умолчанию.

Без опций префиксом становится имя файла.

:prefix задаёт произвольный префикс, в том числе пустой.

use вливает все публичные имена без префикса. Осторожно: при обновлении библиотеки в ней может появиться имя, совпадающее с твоим, и код сломается там, где ты ничего не менял. Разумные применения это собственные модули внутри одного проекта и библиотеки, специально спроектированные под use, например наборы макросов. Для всего остального бери :as.

:export true пробрасывает импортированное дальше, чтобы модуль-агрегатор собрал несколько библиотек под одной крышей:

# lib/all.janet
(import ./calc :as c :export true)
(import ./lib/all :as a)
(a/c/double-it 4)   # 8, добрались через два уровня

import работает только на верхнем уровне

import это макрос: он создаёт связывание во время компиляции файла. Поэтому импортировать внутри функции нельзя, к моменту компиляции её тела связывания ещё нет:

(defn load-lib []
  (import ./lib/calc :as c)
  (c/double-it 2))
# compile error: unknown symbol c/double-it

Если модуль правда нужно подключить по условию во время выполнения, бери функцию require, она возвращает окружение модуля обычным значением:

(defn load-lib []
  (def m (require "./lib/calc"))
  ((get (get m 'double-it) :value) 21))     # 42

Обращение через (get (get env 'name) :value) громоздко, и это намеренно: отложенная загрузка нужна редко. В обычном коде импорты стоят наверху файла. А если она всё же нужна регулярно, заведи обёртку:

(defn from-module [env name]
  (get (get env name) :value))

(def m (require "./lib/calc"))
((from-module m 'double-it) 21)     # 42

Заодно обёртка подсказывает главную мысль следующих разделов: окружение модуля это обычная таблица.

Приватные определения

Суффикс - делает имя приватным: оно работает внутри модуля, но наружу не экспортируется.

Снаружи inner не видна, но triple-it работает: приватность ограничивает экспорт, а не вызовы.

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

Модуль это таблица

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

(def env (require "./lib/sample"))
(type env)     # :table

(get env 'public-fn)
# @{:doc "(public-fn x)\n\nДок публичной."
#   :value <function public-fn>
#   :source-map (...)}

Ключи, которые встречаются на практике:

КлючСмысл
:valueсамо значение
:docдокстрока
:privatetrue у приватных имён
:macrotrue у макросов
:source-mapфайл, строка, столбец
:refмассив-ячейка у var

Отсюда работает (doc name): он просто читает :doc из связывания. Заметь, что в :doc лежит не только твой текст, но и строка вызова, поэтому (doc ...) показывает сигнатуру, хотя ты её не писал: Janet добавил её из списка параметров.

В таблице есть и несимвольные ключи, :current-file и :source. Перебирая окружение, их надо отфильтровывать:

(seq [[name binding] :pairs env :when (symbol? name)] name)

Именно эта интроспекция и делает систему модулей Janet необычной: тебе не нужен особый API, чтобы узнать состав модуля, достаточно обойти таблицу.

Модуль выполняется один раз

import не просто читает файл, он его выполняет, а результат кладёт в module/cache. Повторный импорт берёт готовое:

# lib/noisy.janet
(print "загружаюсь")
(import ./lib/noisy :as n1)    # напечатает "загружаюсь"
(import ./lib/noisy :as n2)    # тихо, взято из кэша

Практические следствия:

  • Код верхнего уровня модуля это код инициализации. Он выполнится ровно один раз, при первом импорте.
  • Циклический импорт это ошибка загрузки: Janet обнаружит петлю и упадёт с circular dependency ... detected. Разрывается петля выносом общего кода в третий модуль, который импортируют оба.
  • Во время разработки перезагрузить модуль можно, удалив его запись из module/cache.

Ключ в кэше это не то, что ты написал в import, а разрешённый путь до файла. Поэтому сначала подсмотри его:

(keys module/cache)                        # @["lib/noisy.janet" ...]
(put module/cache "lib/noisy.janet" nil)
(import ./lib/noisy :as n3)                # снова напечатает "загружаюсь"

Как import находит файл

Что происходит, когда import получает строку:

  1. Путь с ./ или ../ считается от файла, в котором написан import. Поиск на этом заканчивается: либо файл есть, либо ошибка.
  2. Всё остальное идёт по module/paths, массиву шаблонов пути. Janet перебирает их по порядку, подставляя в каждый имя модуля.
  3. Побеждает первый шаблон, указавший на существующий файл или уже загруженный модуль.
  4. Тип найденного записан прямо в шаблоне: :source это .janet-файл, :native скомпилированная C-библиотека, :image образ, :preload уже загруженный.

Шаблон это не абстракция, его можно посмотреть (номер элемента в другой версии Janet может отличаться):

(module/paths 6)
# (":cur:/:all:.janet" :source <function check-relative>)

:cur: заменяется на каталог текущего файла, :all: на имя модуля, :sys: на системный каталог из (dyn :syspath). Последний переопределяется переменной окружения JANET_PATH:

JANET_PATH=/path/to/libs janet program.janet

Правило простое: пути с ./ и ../ ищутся относительно файла, все остальные по module/paths.

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

  • Любой файл это модуль, объявлять ничего не нужно. Путь с ./ считается от файла с import.
  • :as основная форма, use опасен коллизиями, :export true собирает агрегатор.
  • import работает только на верхнем уровне, потому что это макрос над окружением. Для рантайма есть require.
  • Суффикс - делает имя приватным. Приватность ограничивает экспорт, а не вызовы.
  • Модуль после загрузки это таблица связываний с метаданными, доступная для обхода.
  • Модуль выполняется один раз, дальше берётся из module/cache. Перезагрузка это put ключа-пути в nil.
  • Поиск задаётся module/paths и переменной JANET_PATH. Циклический импорт падает с ошибкой при загрузке.

Упражнения

Дальше

Модуль разобран. Следующий урок про то, как из набора файлов сделать пакет: project.janet, зависимости, сборка и публикация.

домашка

Домашка