Мультиметоды в Кложе
Возможно, я упоминал, что недавно сменил проект и теперь пишу на Кложе. Это современный диалект Лиспа под Джава-машину. Кложа довольно проста, и многие задачи в ней решаются исключительно функциями и коллекциями. Так, на собеседованиях в проект я беседовал с разработчиками, которые ничего кроме словарей и коробочных функций не знали.
Не скажу, чтобы это плохо: язык должен быть простым. Чем меньше правил и бест-практик нужно помнить, тем легче на нем писать. Однако, есть в Кложе классные штуки, точечное применение которых сэкономит время и объем кода.
С сегодняшнего для я решил коротко описывать Кложные фишки, на изучение которых вечно не хватает времени. В сегодняшнем выпуске речь пойдет о мультиметодах. Но сначала коротко о том, что такое мультиметоды вообще.
Мы знаем, что ООП базируется на инкапсуляции, наследовании и полиморфизме. Рассмотрим последний. Полиморфизм – это когда у метода может быть несколько реализаций. Конкретная реализация выбирается в зависимости от типов параметров.
В классическом ООП нам бы привели такой пример. У класса Geometry
есть метод
square для вычисления фигур. На вход могут подать окружность, квадрат и
треугольник. Запишу на каком-то выдуманном языке:
class Geometry:
real square(Circle c):
return Math.PI * c.radius * c.radius;
real square(Rectangle rect):
return rect.a * rect.b;
real square(Triangle tri):
real p = (tri.a + tri.b + tri.c) / 2;
return Math.sqrt(p * (p - tri.a) * (p - tri.b) * (p - tri.c))
В зависимости от типа фигуры подбирается нужный метод. Не может быть два метода
с одинаковым набором параметров, даже если их имена различаются. Если передать
совершенно другой объект, например, Rombus
, будет ошибка, причем еще на этапе
компиляции.
Уверен, каждый помнит это с университетских времен. Но полиморфизм на типах – всего лишь частный случай мультиметодов. В отличии от полиморфизма, мультиметоды не прибиты гвоздями к ООП, а значит, дают большую свободу в идеи и реализации.
Для адептов ООП это, возможно, прозвучит сюпризом, но мультиметоды существуют во многих языках, в т.ч. в которых объектов не существует. Почти любой функциональный язык поддерживает множественные клозы (clause) для функций. Но если в ООП все сводится к типам, то в функциональных языках действует более мощный механизм подбора основанный на паттерн-матчинге.
Рассмотрим Хаскель. То, что выше я записал на выдуманном языке, в Хаскеле будет так:
data Circle = Circle Float
data Rectangle = Rectangle Float Float
data Triangle = Triangle Float Float Float
square :: Circle -> Float
square (Circle r) = 3.1415 * r * r
square :: Rectangle -> Float
square (Rectangle a b) = a * b
square :: Triangle -> Float
square (Triangle a b c) = sqrt $ p * (p - a) * (p - b) * (p - c)
where
p = (a + b + c) / 2
Видно, что функция square
работает с типами окружность, прямоугольник,
треугольник. В любой момент мы можем дополнить ее новой фигурой.
Есть мультиметоды и в классических диалектах Лиспа. Вот, например, копипаста из Википедии, взятая из кода Астероидов:
(defmethod collide-with ((x asteroid) (y asteroid))
;; deal with asteroid hitting asteroid
)
(defmethod collide-with ((x asteroid) (y spaceship))
;; deal with asteroid hitting spaceship
)
(defmethod collide-with ((x spaceship) (y asteroid))
;; deal with spaceship hitting asteroid
)
(defmethod collide-with ((x spaceship) (y spaceship))
;; deal with spaceship hitting spaceship
)
Особенность примера с Лиспом в том, что второй параметр в каждой паре ведет себя
как предикат. В данном случае тип spaceship
срабатывает как проверка того, что
x
– экземпляр космического корабля. Однако, вместо spaceship
можно передать
другие проверки: integer?
, string?
, even?
, словом, любой унарный предикат.
Столь гибкая система мультиметодов позволяет определить разное поведение для одного и того же типа, но разных значений. Например, для отрицательной суммы денег одно поведение, для нуля – второе, для положительной – третье. Для даты: выходной или нет, високосный год или нет и т.д.
Думаю, ясно теперь, чем это выгодней банального полиморфизма на типах.
В Кложе более гибкая система мультиметодов. В рассмотренных выше примерах мы не могли изменить сам принцип подбора метода, или диспатча. В Кложе, наоборот, вы обязаны определить диспатч! Рассмотрим пример:
(defmulti foo class)
Данное определение говорит: прежде чем искать реализацию, метод получит класс аргумента.
Следующее определение расширяет мульти-метод реализацией для Long
: если
передано длинное целое, получим строку "an integer"
.
> (defmethod foo Long [x] "an integer")
#multifn[foo 0x478c7d41]
> (foo 42)
"an integer"
Добавим для строки:
> (defmethod foo String [x] (format "%s is a string" x))
#multifn[foo 0x478c7d41]
> (foo "test")
"test is a string"
Если ни одно соответствие не подошло, будет ошибка:
> (foo nil)
IllegalArgumentException No method in multimethod 'foo' for dispatch value: null
clojure.lang.MultiFn.getFn (MultiFn.java:156)
Не страшно, добавим реализацию по умолчанию:
> (defmethod foo :default [x] (format "you passed %s" x))
> (foo nil)
"you passed null"
Диспатч может работать самым разным способом, например, проверять на типы все аргументы. Повторим пример с площадью фигур:
(defmulti square (fn [& args] (mapv class args)))
;; rectangle
(defmethod square [Long Long] [a b] (* a b))
(square 2 3) ;; 6
;; circle
(defmethod square [Double] [r] (* Math/PI r r))
(square 1.1) ;; 3.8013271108436504
;; triangle
(defmethod square [Long Long Long]
[a b c]
(let [p (/ (+ a b c) 2)]
(Math/sqrt (* p (- p a) (- p b) (- p c)))))
(square 2 2 2) ;; 1.7320508075688772
Замечу, что на самом деле Кложа проверяет значения диспатча и образца не простым
сравнением, а функцией isa?
, что подразумевает иерархию. Так, чтобы проверку
проходили типы PersistentArrayMap
и PersistentHashMap
, достаточно указать
базовый класс clojure.lang.APersistentMap
.
Наконец, проверять можно не только на типы, а банально на значения, диапазоны, предикаты и тд. В текущем проекте я проверяю последний переданный аргумент. Это может быть как айдишка, так и запись БД. В зависимости от этого условия срабатывает разная логика.
Каждый мультиметод может вызвать внутри другой, тогда процесс диспатча начнется заново.
Мультиметоды в Кложе отличаются еще и тем, что расширяемы извне. Это значит, если разработчик определил мультиметод для определенных типов, ничто не мешает расширить его до других типов. И это будет все тот же экземпляр.
Напомню ситуацию с примерами на Джаве и Хаскеле выше. Пусть фукция square
и
класс Geometry
находятся в чужих библиотеах. Тогда вы никак не сможете
изменить их! В лучшем случае вы унаследуете класс Geometry
, добавите свой
метод для ромба. Но если есть еще одна библиотека, которая работает с
Geometry
, вы никак не сможете на это повлиять.
Напротив, идея расширения чужих определений работает в Кложе просто убийственно. Я нигде не видел ничего подобного.
В Кложе мультиметоды используют когда трудно предугадать, какие типы данных будут поступать на вход. Это прекрасное решение для абстракций вроде работы с БД, парсингом данных, декораторов и тд.
Мультиметоды в Лиспах – мощнейшей инструмент, порой в корне меняющий принцип мышления и разработки.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter