Множественная диспетчеризация — сердце Julia

Главная парадигма Julia — выбор реализации функции по типам сразу всех её аргументов.

Множественная диспетчеризация (multiple dispatch) — механизм, при котором конкретная реализация функции выбирается на основе типов всех переданных аргументов, а не только одного объекта.

В чём идея

В объектно-ориентированных языках вроде Python метод привязан к одному объекту: cat.speak() — реализация выбирается по типу cat. Это «одиночная диспетчеризация» — решение по одному (первому) аргументу. В Julia функция — это набор методов, и нужный метод выбирается по комбинации типов всех аргументов.

Несколько методов одной функции

Определим функцию interact для разных пар животных:

interact(a, b) = "$a и $b просто рядом"
interact(a::String, b::Int) = "строка $a повторяется $b раз"

println(interact("кот", "пёс"))
println(interact("ха", 3))

Вывод:

кот и пёс просто рядом
строка ха повторяется 3 раз

Запись a::String означает «этот метод подходит, только если a — строка». Julia сама выбирает самый специфичный подходящий метод по типам аргументов.

Классический пример: операторы

Само сложение + в Julia — функция с множеством методов: один для двух Int, другой для двух Float64, третий для Int и Float64, для комплексных, для матриц и т. д. Когда вы пишете 2 + 3.0, диспетчеризация выбирает метод для пары (Int, Float64). Вы можете добавить свой метод + для собственных типов — и встроенный синтаксис заработает с ними.

Почему это мощно

Множественная диспетчеризация решает «проблему выражения»: легко добавлять и новые типы, и новые операции над существующими типами, не меняя чужой код. Поэтому пакеты Julia так хорошо сочетаются: автор библиотеки чисел и автор библиотеки графиков могут не знать друг о друге, но их типы будут работать вместе, если для нужных комбинаций определены методы.

Как работает под капотом

При вызове функции Julia смотрит на типы аргументов и в так называемой таблице методов находит самый специфичный из подходящих. В большинстве случаев типы известны на этапе компиляции, поэтому выбор метода происходит не во время выполнения, а заранее — это и быстро, и порождает специализированный машинный код. Посмотреть все методы функции можно командой methods(+) — для сложения их будут сотни.

Частые ошибки

Главное заблуждение пришедших из ООП — пытаться воссоздать классы и наследование «методов внутри объекта». В Julia нет методов, принадлежащих типу; есть свободные функции с методами под разные типы. Не пишите obj.method() — пишите method(obj). Вторая ошибка — определить слишком общий метод f(a, b), который перехватывает вызовы, предназначенные более специфичным методам из других пакетов; будьте конкретны в аннотациях типов там, где это важно.

Итоги

  • Множественная диспетчеризация выбирает метод по типам всех аргументов.
  • Это отличается от ООП-диспетчеризации по одному объекту (obj.method()).
  • Функция — набор методов; + сам реализован множеством методов.
  • Подход позволяет легко расширять и типы, и операции — пакеты Julia хорошо сочетаются.
  • Выбор метода обычно происходит на этапе компиляции, что даёт скорость.
Проверьте себя
1. По чему множественная диспетчеризация выбирает конкретный метод функции?
AТолько по типу первого аргумента
BПо типам всех переданных аргументов
CПо имени переменной, которой присвоен результат
DПо порядку определения методов
2. Как в Julia принято вызывать поведение для объекта вместо obj.method()?
Aobj->method
Bmethod(obj) — как свободную функцию
Cobj::method
DНикак, нужны классы