Ad-hoc полиморфизм, перегрузка и субтипизация

Три разных явления под одним словом «полиморфизм» — и почему их важно не путать.

Ad-hoc полиморфизм — один и тот же интерфейс с разными реализациями для разных типов; субтипизация — возможность использовать подтип там, где ждут супертип.

Три вида полиморфизма

ВидСутьПример
Параметрическийодин код для всех типовlength, generics
Ad-hoc (перегрузка)разный код под каждый тип+ для Int и для String, classes
Субтипизацияподтип годится вместо супертипаCat <: Animal, ООП-наследование

Параметрический мы разобрали: a непрозрачен, поведение единообразно. Ad-hoc — противоположность: show 5 и show "hi" выполняют разный код, хотя имя одно. Субтипизация — третья ось: если Cat — подтип Animal (пишут Cat <: Animal), то значение Cat можно подставить туда, где ждут Animal.

Ad-hoc: перегрузка и диспетчеризация

Перегрузка выбирает реализацию по типу. В C++/Java — по сигнатуре на этапе компиляции; в Haskell — через классы типов (отдельный урок) и словари; в динамических языках — по типу в рантайме. Ключевое отличие от параметрического: функция знает тип и ведёт себя по-разному.

Субтипизация и принцип подстановки

Подтипизация задаётся отношением S <: T («S — подтип T»). Правило подстановки: значение типа S можно использовать везде, где ожидается T. Тонкое место — вариантность функций. Если Cat <: Animal, то функция Animal -> X является подтипом Cat -> X (а не наоборот!): по аргументу стрелка контравариантна, а по результату — ковариантна.

Cat <: Animal

по результату (ковариантно):   (X -> Cat)    <:  (X -> Animal)
по аргументу (контравариантно):(Animal -> X) <:  (Cat -> X)

Демонстрация: подстановка и контравариантность аргумента

Смоделируем иерархию Cat <: Animal и проверим: функция, принимающая любого Animal, безопасно работает там, где нужна функция над Cat (контравариантность аргумента), но не наоборот.

subtype = {"Cat": "Animal", "Dog": "Animal"}   # Cat <: Animal, Dog <: Animal
def is_sub(s, t):
    return s == t or subtype.get(s) == t

# функция over Animal принимает и Cat (подстановка работает)
def feed(animal_kind):
    return "кормлю как Animal: " + animal_kind

print("feed(Cat)  :", feed("Cat"))          # Cat годится там, где ждут Animal
print("Cat <: Animal      :", is_sub("Cat", "Animal"))
print("Animal <: Cat      :", is_sub("Animal", "Cat"))   # неверно
# контравариантность: (Animal -> X) можно дать туда, где нужна (Cat -> X)
print("(Animal->X) <: (Cat->X):", is_sub("Cat", "Animal"))

Вывод:

feed(Cat)  : кормлю как Animal: Cat
Cat <: Animal      : True
Animal <: Cat      : False
(Animal->X) <: (Cat->X): True

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

Субтипизация усложняет вывод типов: вместо равенства типов (унификация) появляются неравенства S <: T, и решать приходится систему ограничений-подтипов, что тяжелее. Поэтому языки с богатым выводом (ML, Haskell) исторически избегали структурной подтипизации, делая ставку на параметрический полиморфизм и классы типов. TypeScript, наоборот, строит всё на структурной подтипизации — и платит более слабым выводом и сложной вариантностью.

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

  • Считать наследование единственным полиморфизмом. ООП-наследование — лишь субтипизация; есть ещё параметрический и ad-hoc.
  • Перепутать вариантность. Аргумент функции контравариантен; наивное «всё ковариантно» ломает безопасность (классическая дыра с массивами в ранней Java).
  • Смешивать перегрузку и параметрический полиморфизм. Перегрузка зависит от типа и делает разное; параметрическая функция типа не видит.

Итоги

  • Три полиморфизма: параметрический (один код), ad-hoc (разный код), субтипизация (подтип вместо супертипа).
  • Перегрузка/классы типов выбирают реализацию по типу — это ad-hoc.
  • Стрелка контравариантна по аргументу и ковариантна по результату.
  • Субтипизация усложняет вывод (неравенства вместо равенств), отсюда выбор языков.
Проверьте себя
1. Чем ad-hoc полиморфизм отличается от параметрического?
AНичем
BAd-hoc выполняет разный код под каждый тип, параметрический — один код для всех
CAd-hoc быстрее
DПараметрический видит тип
2. Как соотносятся типы функций при Cat <: Animal?
A(Animal->X) <: (Cat->X): аргумент контравариантен
B(Cat->X) <: (Animal->X)
CОни равны
DНесравнимы
3. Почему языки с сильным выводом типов избегают богатой субтипизации?
AОна некрасива
BПодтипизация даёт неравенства S <: T, решать их труднее, чем равенства
CОна запрещена стандартом
DНет причин