The Expression Problem and its solutions (2016)
Проблема выражений и её решения
Проблема выражений: нужно добавлять новые типы данных и новые операции без изменения старого кода.
В ООП-языках легко добавлять типы (наследование), но сложно — операции (менять интерфейс).
В функциональных языках наоборот: легко добавлять функции, сложно — варианты данных.
Пример на C++
Базовый класс Expr
с двумя методами: Eval()
и ToString()
.
Новый тип — просто новый класс-наследник.
Новая операция — правим базовый класс и все наследников, нарушая OCP.
Функциональный подход (Haskell)
Типы данных и функции разведены:
data Expr = Constant Double | BinaryPlus Expr Expr
eval (Constant x) = x
eval (BinaryPlus a b) = eval a + eval b
Добавить операцию легко: пишем новую функцию.
Добавить вариант Expr
— правим сам тип и все функции, pattern-match’и которых его затрагивают.
Как быть
- Визитор (ООП) — двойная диспетчеризация, но код всё равно растёт.
- Мультиметоды (CLOS, Clojure) — выбор по типу всех аргументов, код не трогается.
- Type-class / протоколы (Haskell, Clojure) — «открытые» функции, реализуемые вне исходного модуля.
- Tagless-final / finally-tagless — выразить язык как набор операций, интерпретаторы добавляются без изменения AST.
Итог: ни один стиль не побеждает; выбираем язык и технику, которая даёт нужную сторону расширяемости.