Полиморфизм: мультиметоды и протоколы
Узнаём, как Clojure достигает полиморфизма без классов и наследования.
Полиморфизм — способность одной операции вести себя по-разному в зависимости от данных, к которым она применяется.
Мультиметоды: диспетчеризация по чему угодно
В ООП метод выбирается по типу объекта. Мультиметоды Clojure гибче: вы сами задаёте функцию диспетчеризации, которая по аргументам вычисляет ключ, а реализация выбирается по этому ключу. Диспетчеризовать можно по типу, по полю map, по чему угодно.
; диспетчеризация по значению ключа :тип
(defmulti площадь :тип)
(defmethod площадь :круг [ф]
(* 3.14 (:r ф) (:r ф)))
(defmethod площадь :квадрат [ф]
(* (:сторона ф) (:сторона ф)))
(площадь {:тип :круг :r 10}) ; => 314.0
(площадь {:тип :квадрат :сторона 5}) ; => 25Вывод:
314.0 25
Чтобы добавить новую фигуру, не нужно трогать существующий код — достаточно написать новый defmethod. Это открытое расширение.
Протоколы: быстрые наборы методов
Когда диспетчеризация нужна именно по типу и важна скорость, используют протоколы. Протокол — это набор имён функций (как интерфейс), а реализация задаётся для конкретных типов.
(defprotocol Звук
(издать [сущ]))
(defrecord Кошка []
Звук
(издать [_] "Мяу"))
(defrecord Собака []
Звук
(издать [_] "Гав"))
(издать (->Кошка)) ; => "Мяу"
(издать (->Собака)) ; => "Гав"Вывод:
"Мяу" "Гав"
Мультиметоды против протоколов
| Мультиметоды | Протоколы | |
| Диспетчеризация | по любому признаку | только по типу |
| Скорость | медленнее | очень быстрая (как Java-вызов) |
| Гибкость | максимальная | как у интерфейсов |
| Когда выбрать | сложная логика выбора | обычный полиморфизм по типу |
Как работает под капотом
Мультиметод хранит таблицу «ключ → реализация». При вызове он сначала зовёт функцию диспетчеризации, получает ключ и ищет в таблице нужный метод (с учётом иерархий). Протокол же компилируется в Java-интерфейс, а defrecord создаёт класс, реализующий его, поэтому вызов метода протокола — это прямой быстрый вызов JVM, почти без накладных расходов.
Частые ошибки
- Брать мультиметоды, когда хватает протокола. Для простой диспетчеризации по типу протокол быстрее и яснее.
- Забыть
defmethodпо умолчанию. Если ни один ключ не подошёл, мультиметод бросит ошибку; добавьте:default-реализацию. - Думать, что нужны классы и наследование. Clojure достигает полиморфизма без иерархий классов.
Итоги
- Полиморфизм в Clojure — без классов и наследования.
- Мультиметоды диспетчеризуют по любому вычисляемому признаку; их легко расширять.
- Протоколы — быстрый полиморфизм по типу, компилируются в Java-интерфейсы.
- Для простого случая по типу берут протокол, для сложной логики — мультиметод.