Решаемая задача: реализовать диспетчеризацию по типу своими руками.
Разложим весь процесс на примере библиотеки для работы с геометрическими фигурами. Предположим, что мы можем создавать разные фигуры, такие как треугольник, круг или квадрат. Кроме специфических свойств, у фигур есть и общие, например, периметр или площадь. А так как мы, гипотетически, хотим работать с фигурами единообразно, то реализуем диспетчеризацию по типу на примере функции, вычисляющей общую площадь фигур, размещенных на воображаемом холсте (так обычно называется область, на которой происходит рисование в графических редакторах)
При отсутствии готовой диспетчеризации нам придется делать ее руками в том месте, где требуется обобщенное поведение:
import { reduce } from 'js-pairs-data';
import * as circle from './circle';
import * as square from './square';
import * as triangle from './triangle';
import { typeTag } from './type';
const getTotalArea = figures => reduce((figure, total) => {
let area;
switch (typeTag(figure)) {
case 'square':
area = square.getArea(figure);
break;
case 'circle':
area = circle.getArea(figure);
break;
case 'triangle':
area = triangle.getArea(figure);
break;
};
return area + total;
}, 0, figures);
С наличием автоматического механизма диспетчеризации (не важно реализован он в самом языке или нами самостоятельно) код сокращается до следующего:
import * as circle from './circle';
import * as square from './square';
import { reduce, l } from 'js-pairs-data';
import { getArea } from './figures';
const getTotalArea = figures => reduce((figure, total) => getArea(figure) + total, 0, figures);
const figures = l(circle.make(2), square.make(3));
getTotalArea(figures);
// 12.57 + 9
// 21,57
В примере выше функция getArea
сама по себе не занимается вычислением площади. Это вычисление
реализовано для каждой фигуры совершенно независимо. Все, что делает getArea
, это перенаправляет
запрос на расчет площади в соответствующую функцию.
Алгоритм диспетчеризации в примере выше следующий:
getArea
извлекает тип (его название) из фигуры.getArea
обращается к глобальному хранилищу (виртуальная таблица) для поиска нужной реализации
настоящей функции вычисления площади.getArea
ее вызывает с нужными аргументами и возвращает результат наружу.Важное следствие этого алгоритма в том, что для работы автоматической диспетчеризации необходимо, чтобы
реальные функции getArea
были занесены в виртуальную таблицу, иначе до них невозможно будет достучаться.
Выполняет две задачи, которые мы рассмотрим ниже.
Первая задача — это регистрация функций тех типов, по которым мы планируем делать диспетчеризацию:
export const definer = type => (methodName, f) => { /* ... */ };
Тогда модуль, реализующий наш тип, будет выглядеть так:
// circle.js
import { definer } from './generic';
import { attach, contents } from './type';
const defmethod = definer('Circle');
export const make = radius => attach('Circle', radius);
// Так как для определения круга не нужно ничего кроме радиуса, сам круг и есть радиус,
// Код снаружи об этом не знает!
export const getRadius = circle => contents(circle);
export const getArea = circle => (getRadius(circle) ** 2) * Math.PI;
defmethod('getArea', getArea);
export const getPerimeter = circle => 2 * getRadius(circle) * Math.PI;
defmethod('getPerimeter', getPerimeter);
Как видно из примера выше, по большей части Circle
является типичной абстракцией, за исключением
пары моментов:
definer
происходит регистрация нужных (радиус специфичен для круга, по нему диспетчеризация
не нужна) функций в нашей виртуальной таблице.Наш модуль generic
ничего не знает про Circle
, да и вообще ничего не знает про тех, кто его использует.
В общем случае, для регистрации функции ему нужно знать три значения: имя типа, имя функции и само тело
функции, или, другими словами, мы имеем такой интерфейс: register('TypeName', 'funcName', funcBody)
. А код
регистрации выглядел бы так:
register('Circle', 'getArea', getArea);
register('Circle', 'getPerimeter', getPerimeter);
Обратите внимание на то, что мы находимся внутри модуля Circle
и нам приходится в каждом вызове register
передавать его название. Это единственная причина, по которой существует функция defmethod
. То есть мы
сначала специфицируем имя типа для которого будем заполнять функции, а потом делаем это без повторений.
С точки зрения теории мы использовали так называемое частичное применение функции:
const defmethod = partial(register, 'Circle');
Что эквивалентно:
const defmethod = (funcName, funcBody) => register('Circle', funcName, funcBody);
Ну и самое главное, а где же происходит регистрация? Куда записываются все эти данные о типах?
Ответ достаточно простой. Фактически в наш прекрасный чистый код мы вводим внешнее изменяемое
состояние и заполняем его функцией с побочными эффектами (definer
). Если открыть модуль
generic
, то можно увидеть:
let methods = l();
В свою очередь, все функции, которым нужен доступ к таблице, получают его посредством замыкания.
Причем только definer
изменяет ее, а все остальные - читают.
Получается, что methods
наполняется в тот момент, когда загружаются типы (выполняется import
), использующие модуль
generic
для регистрации своих функций. Например:
// Первый встреченный импорт модуля `circle` приведет к тому, что внутри него выполнятся все определения.
import * as circle from './circle';
Вторая задача это, собственно, поиск этих функций:
// извлечение типа объекта происходит внутри с помощью typeTag
export const getMethod = (obj, funcName) => { /* ... */ };
Для поиска подходящей функции достаточно знать два параметра: имя типа и имя функции. Если
функция найдена, то getMethod
возвращает ее вызывающему коду, который, в свою очередь,
уже делает вызов найденной функции.
// figures.js
import { getMethod } from './generic';
import { contents } from './type';
export const getArea = (figure) => {
const realGetArea = getMethod(figure, 'getArea');
// В случае с кругом эквивалентно:
// circle.getArea(figure)
return realGetArea(figure);
}
Вам ответят команда поддержки Хекслета или другие студенты.
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Наши выпускники работают в компаниях:
Зарегистрируйтесь или войдите в свой аккаунт