Полиморфизм: мультиметоды и протоколы

Узнаём, как 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-интерфейсы.
  • Для простого случая по типу берут протокол, для сложной логики — мультиметод.
Проверьте себя
1. По какому признаку мультиметод выбирает реализацию?
AТолько по типу первого аргумента
BПо любому значению, которое вернёт функция диспетчеризации
CПо имени переменной
DСлучайно
2. В чём преимущество протоколов перед мультиметодами?
AОни диспетчеризуют по любому признаку
BОни очень быстры, так как компилируются в Java-интерфейсы
CИх не нужно объявлять
DОни работают только в ClojureScript
3. Как добавить поведение для новой фигуры в мультиметод площадь?
AПереписать defmulti
BДобавить новый defmethod, не трогая существующие
CСоздать класс-наследник
DИзменить функцию диспетчеризации