Прототипы, методы и полиморфизм
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Прототипы, методы и полиморфизм
Возвращаем долг: в третьем уроке мы отметили, что
(:a {:a 1})даётnil, и отложили объяснение. Пора его дать, потому что это не случайность, а отдельный механизм языка.
Вызов метода
(:key obj) это вызов метода. Janet ищет :key в объекте и вызывает найденное значение как функцию, передавая ей сам объект первым аргументом:
Параметр self это и есть объект. По смыслу он соответствует this или self из других языков, но здесь он не магический: обычный первый параметр, который ты обязан объявить сам. Никакого неявного связывания контекста, из-за которого в JavaScript выросла целая тема про this и bind.
Дополнительные аргументы идут следом:
Через self метод меняет объект:
Три случая, которые надо различать
| Ситуация | Результат |
|---|---|
| Ключ есть, значение функция | вызывается, объект первым аргументом |
| Ключ есть, значение не функция | nil, молча |
| Ключа нет | ошибка unknown method |
(:a {:a 1}) # nil, под :a лежит число, вызвать его нельзя
(:missing {:a 1}) # error: unknown method :missing invoked on <struct ...>
(:a 5) # error: unknown method :a invoked on 5
Средняя строка самая опасная. Отсутствующий метод даёт громкую ошибку, и это хорошо. А вот ключ с не-функцией под ним возвращает nil без единого предупреждения.
Именно на это натыкаются пришедшие из Clojure: там (:key dict) стандартный способ достать значение, и код выглядит совершенно нормально. В Janet он тихо вернёт nil и уронит логику где-то дальше.
Для чтения данных бери get или вызов самой структуры: (get dict :key), (dict :key).
Прототипы
Таблице можно назначить прототип, другую таблицу, куда Janet заглянет, если своего ключа не нашлось:
Прототип у прототипа даёт цепочку произвольной длины. Поиск ключа это вопрос, заданный по цепочке: таблица не знает ответа сама, спрашивает родителя, тот своего. Отвечает первый, кто знает, поэтому своё значение всегда заслоняет родительское, и переопределение работает само собой:
Потыкай ключи и посмотри, на каком уровне цепочки они находятся:
Узнать прототип можно через table/getproto, у таблицы без прототипа он nil.
Ловушка: keys и length не видят прототип
Разные функции по-разному относятся к цепочке:
Это логично: иначе each по объекту выдавал бы вперемешку данные и методы. Но забывается это легко. Нужны все доступные ключи, поднимайся по цепочке через table/getproto руками.
Классы и конструкторы
Из прототипов собирается привычная объектная модель. Роль класса играет обычная таблица с методами, а конструктор это метод, создающий экземпляр с собой в качестве прототипа:
Наследование это прототип от прототипа: подкласс получает Point своим прототипом и переопределяет нужные методы.
Никакого отдельного синтаксиса классов в Janet нет, и он не нужен: всё выражается таблицами и одной функцией table/setproto.
Полиморфизм стандартной библиотеки
Прототипы не единственный полиморфизм в языке. Многие функции работают с разными типами:
next это основа всего перебора: each, loop, map построены на нём. Он умеет обходить массивы, таблицы, структуры и строки:
Раз есть next, перебор пишется руками, без each:
Именно этот цикл прячется внутри each, loop и map.
Абстрактные типы, приходящие из C, подключаются к тем же протоколам на стороне C. Файл это значение из мира C, но на нём работает вызов метода из начала этого урока:
(def f (file/open "project.janet"))
(type f) # :core/file
(:read f :line) # первая строка файла, метод у C-значения
(:close f)
С точки зрения твоего кода такие типы ничем не отличаются от встроенных.
Прототипы или замыкания
У прототипов есть альтернатива: замыкание, хранящее состояние в лексической области видимости.
# Через замыкание
(defn make-counter []
(var n 0)
{:increment (fn [] (++ n))
:value (fn [] n)})
# Через прототипы
(def Counter @{:increment (fn [self] (put self :n (inc (self :n))) self)
:value (fn [self] (self :n))})
Замыкания бери, когда объектов немного, состояние действительно приватное, а наследование не нужно. Состояние физически недоступно снаружи, и это надёжнее.
Прототипы бери, когда объектов много (методы хранятся один раз, а не копируются в каждый), нужно наследование или переопределение поведения, либо объект должен быть обычными данными: его можно напечатать, сериализовать через marshal из 31-janet/05 · Значения и ссылки, разобрать в match из 31-janet/14 · Ветвления и match.
Не начинай с классов. Соблазн построить иерархию стоит придержать. Чаще всего хватает обычной структуры данных и набора функций над ней, так код проще тестировать и читать. Прототипы окупаются там, где правда нужен полиморфизм: разные реализации одного интерфейса, выбираемые во время выполнения.
Что запомнить
(:key obj)это вызов метода, объект передаётся первым аргументом.selfэто обычный параметр, объявляемый вручную, а не магическийthis.- Ключ с не-функцией даёт
nilмолча, отсутствующий ключ даёт громкую ошибку. - Для чтения данных бери
get, а не(:key ...). table/setprotoзадаёт прототип, поиск идёт вверх по цепочке до первого совпадения.getиhas-key?идут по цепочке,keysиlengthвидят только своё.- Класс это таблица с методами, конструктор ставит её прототипом экземпляру.
- Замыкания для приватного состояния, прототипы для полиморфизма и множества объектов.
Упражнения
Дальше
Язык пройден целиком. Следующие два урока про границу с C: как вызвать чужой код через FFI и как встроить Janet в свою программу. Оба требуют компилятора C, и при желании их можно отложить.
домашка