Модули: файл, таблица, окружение
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Модули: файл, таблица, окружение
В 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 | докстрока |
:private | true у приватных имён |
:macro | true у макросов |
: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 получает строку:
- Путь с
./или../считается от файла, в котором написанimport. Поиск на этом заканчивается: либо файл есть, либо ошибка. - Всё остальное идёт по
module/paths, массиву шаблонов пути. Janet перебирает их по порядку, подставляя в каждый имя модуля. - Побеждает первый шаблон, указавший на существующий файл или уже загруженный модуль.
- Тип найденного записан прямо в шаблоне:
: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, зависимости, сборка и публикация.
домашка