JS: Программирование, управляемое данными
Теория: Диспетчеризация по типу. Аддитивность.
Решаемая задача: реализовать диспетчеризацию по типу своими руками.
Разложим весь процесс на примере библиотеки для работы с геометрическими фигурами. Предположим, что мы можем создавать разные фигуры, такие как треугольник, круг или квадрат. Кроме специфических свойств, у фигур есть и общие, например, периметр или площадь. А так как мы, гипотетически, хотим работать с фигурами единообразно, то реализуем диспетчеризацию по типу на примере функции, вычисляющей общую площадь фигур, размещенных на воображаемом холсте (так обычно называется область, на которой происходит рисование в графических редакторах)
При отсутствии готовой диспетчеризации нам придется делать ее руками в том месте, где требуется обобщенное поведение:
С наличием автоматического механизма диспетчеризации (не важно реализован он в самом языке или нами самостоятельно) код сокращается до следующего:
В примере выше функция getArea сама по себе не занимается вычислением площади. Это вычисление
реализовано для каждой фигуры совершенно независимо. Все, что делает getArea, это перенаправляет
запрос на расчет площади в соответствующую функцию.
Алгоритм диспетчеризации в примере выше следующий:
getAreaизвлекает тип (его название) из фигуры.getAreaобращается к глобальному хранилищу (виртуальная таблица) для поиска нужной реализации настоящей функции вычисления площади.- Если реализация найдена, то
getAreaее вызывает с нужными аргументами и возвращает результат наружу.
Важное следствие этого алгоритма в том, что для работы автоматической диспетчеризации необходимо, чтобы
реальные функции getArea были занесены в виртуальную таблицу, иначе до них невозможно будет достучаться.
Виртуальная таблица
Выполняет две задачи, которые мы рассмотрим ниже.
Регистрация
Первая задача — это регистрация функций тех типов, по которым мы планируем делать диспетчеризацию:
Тогда модуль, реализующий наш тип, будет выглядеть так:
Как видно из примера выше, по большей части Circle является типичной абстракцией, за исключением
пары моментов:
- Внутри создается привязка к типу. Соответственно все селекторы должны сначала извлечь данные и потом уже работать.
- С помощью
definerпроисходит регистрация нужных (радиус специфичен для круга, по нему диспетчеризация не нужна) функций в нашей виртуальной таблице.
Наш модуль generic ничего не знает про Circle, да и вообще ничего не знает про тех, кто его использует.
В общем случае, для регистрации функции ему нужно знать три значения: имя типа, имя функции и само тело
функции, или, другими словами, мы имеем такой интерфейс: register('TypeName', 'funcName', funcBody). А код
регистрации выглядел бы так:
Обратите внимание на то, что мы находимся внутри модуля Circle и нам приходится в каждом вызове register
передавать его название. Это единственная причина, по которой существует функция defmethod. То есть мы
сначала специфицируем имя типа для которого будем заполнять функции, а потом делаем это без повторений.
С точки зрения теории мы использовали так называемое частичное применение функции:
Что эквивалентно:
Ну и самое главное, а где же происходит регистрация? Куда записываются все эти данные о типах?
Ответ достаточно простой. Фактически в наш прекрасный чистый код мы вводим внешнее изменяемое
состояние и заполняем его функцией с побочными эффектами (definer). Если открыть модуль
generic, то можно увидеть:
В свою очередь, все функции, которым нужен доступ к таблице, получают его посредством замыкания.
Причем только definer изменяет ее, а все остальные - читают.
Получается, что methods наполняется в тот момент, когда загружаются типы (выполняется import), использующие модуль
generic для регистрации своих функций. Например:
Поиск
Вторая задача это, собственно, поиск этих функций:
Для поиска подходящей функции достаточно знать два параметра: имя типа и имя функции. Если
функция найдена, то getMethod возвращает ее вызывающему коду, который, в свою очередь,
уже делает вызов найденной функции.

