Почему конструкция if-then-else малополезна в языках программирования
Введение: if-then-else⌗
В большинстве языков программирования имеется условная конструкция IF ... THEN ... ELSE ...
.
Она знакома любому школьнику и представляет собой развилку на пути из некоторого абстрактного типа a
в тип b
.
Решение о том, взять первый путь или второй, принимается с помощью предиката, то есть путя из a
в булев тип
(истина/ложь). Вместе с обоими вариантами b
(т.н. ветками условия), предикат также идёт параметром к конструкции
IF ... THEN ... ELSE ...
:
$$
ite(pred, b1, b2, a) =
\begin{cases}
b1, &\quad pred(a) = true \
b2, &\quad pred(a) = false
\end{cases}
$$
Паттерн-матчинг⌗
В правой части формулы выше выполняется паттерн-матчинг против конструкторов типа pred
: собран ли результат
pred(a)
конструктором true
или конструктором false
?
Haskell предоставляет простой синтаксис для паттерн-матчинга конструкторов и, как мы скоро увидим, данный синтаксис –
это единственная встроенная в язык конструкция ветвления. Условная конструкция case ... of ...
является более
общей, чем конструкция if ... then ... else ...
и поэтому используется повсеместно. А вот if
, по моим наблюдениям,
программисты применяют неохотно.
Формула выше в переводе на язык Haskell выглядит следующим образом:
ifThenElse :: (a -> Bool) -> b -> b -> (a -> b)
ifThenElse pred b1 b2 a =
case pred a of
True -> b1
False -> b2
Из сигнатуры видно, что функция возвращает стрелку (путь) a -> b
. Функции даётся предикат над a
и два варианта
типа b
(первый вариант соответствует истине предиката).
Аналогичная нашей функции ite
конструкция if ... then ... else ...
уже присутствует в Haskell, но является лишь
синонимом (синтаксическим сахаром) соответствующей клаузы case .. of
:
ifThenElse' pred t f a = if pred a then t else f
Вот так мы используем условную конструкцию:
> ifThenElse (> 10) "big" "small" 6
"small"
> ifThenElse (> 10) "big" "small" 26
"big"
> :t ifThenElse (> 10) "big" "small"
ifThenElse (> 10) "big" "small" :: (Ord a, Num a) => a -> [Char]
На последней строчке мы выясняем конкретный тип “заполненной” конструкции: это путь из любого упорядоченного числового типа в список символов (т.е. в строку).
Изоморфные Bool типы⌗
Вообще же, вместо Bool
мы можем использовать для if ... then ... else ...
любой изоморфный тип. Пускай имеется
следующий тип-сумма:
data B = Constructor1 | Constructor2
Нетрудно заметить “похожую” на Bool
форму: ровно два конструктора.
Определим тайпкласс изоморфных структур и составим изоморфизм между типами B
и Bool
:
class Isomorphic a b where
forward :: a -> b
inverse :: b -> a
toBool :: B -> Bool
toBool Constructor1 = True
toBool Constructor2 = False
fromBool :: Bool -> B
fromBool True = Constructor1
fromBool False = Constructor2
instance Isomorphic B Bool where
forward = toBool
inverse = fromBool
Теперь мы можем использовать нуше конструкцию if ... then ... else ...
с любым похожим на Bool
типом c
:
ifThenElse :: Isomorphic c Bool => (a -> c) -> b -> b -> (a -> b)
ifThenElse pred t f a =
case (forward . pred) a of
True -> t
False -> f
Ограниченность if-then-else⌗
Обобщая далее на три и более конструкторов, становится ясно, что куда удобнее читать код с одной единственной наиболее общей конструкцией case .. of ...
, чем с целым деревом вложенных друг в друга конструкций if ... then ... else ...
(если A = X то B1 иначе если A = Y то B2 иначе B3 и т.п.).
Как мы уже выяснили, конструкция if ... then ... else ...
сводится к паттерн-матчингу предиката (типа Bool
), а не
центрального типа (скажем, a
), который фактически и играет главную роль в процессе выбора пути из a
. Другими
словами, каждое употребление выражения if ... then ... else ...
с небулевым центральным типом привносит
семантически необоснованную зависимость одного типа от другого.
Представим, что у нас есть следующий тип:
data Direction = Left | Right
Хотя данный тип и похож (изоморфен) на Bool
, семантически концепция направления не имеет ничего общего с концепцией
истинности. Именно поэтому компилятор не примет следующее уравнение:
f :: Direction -> String
f d = "Going " ++ if d == Left then "left" else "right"
Дело в том, что предикат, то есть функция (==)
определена только для
сравнимых типов (тайпкласс Eq
), а наш тип направления, в общем-то, таковым
не является. И быть совершенно не обязан, хотя теоретически мы и могли бы
определить, когда два направления считать эквивалентными (например, когда они собраны одинаковыми конструкторами).
Универсальность case … of …⌗
Окей, мы понимаем и хотим использовать в программе данную идею: два направления равны, когда они собраны одинаковыми
конструкторами. Однако причём здесь Bool
и предикаты??? Ведь мы не сравниваем два направления в функции f
, а
соотносим каждому направлению строку! И по смыслу не претендуем ни на что более. Используя общий паттерн-матчинг case ... of ...
мы можем запросто выполнить задачу напрямую без всяких дополнительных зависимостей:
f' :: Direction -> String
f' d = "Going " ++ case d of
Left -> "left"
Right -> "right"
Или даже просто дав компилятору несколько уравнений, по одному на каждый конструктор:
f'' :: Direction -> String
f'' Left = "Going left"
f'' Right = "Going right"
Становится видно, что использование if ... then ... else ...
в нашем отдельном случае фактически усложняет
процесс: с данной конструкцией мы можем паттерн-матчинг только по True
или False
, а значит нам необходимо установить
некоторую минимальную (мысленную) связь типа Direction
с типом Bool
. Представим, что if ... then ... else ...
-
это единственная конструкция ветвления в нашем языке. Нам необходимо каким-то образом связывать с булевым типом все прочие типы нашей программы, что не разумно.
Куда круче иметь в своём арсенале лишь case .. of ...
и самому написать упрощённую конструкцию ветвления, если
таковая вообще потребуется (синтаксис вполне сравним с if ... then ... else ...
по читабельности: f' d = "Going " ++ case d of Left -> "left"; Right -> "right"
).
Мне кажется, куда проще принимать за отправную точку идею паттерн-матчинга, чем идею эквивалентности предикатов.
Комбинаторы when и unless⌗
Думая в ключе функций высшего порядка, мы запросто можем написать дополнительные вырожденные комбинаторы
наподобие if
, делающие код программы на Haskell ещё более читабельным.
Два хороших примера - это when
и unless
из Control.Monad
, встраивающие в аппликативный контекст свой первый
либо второй аргумент:
when :: Applicative f => Bool -> f () -> f ()
unless :: Applicative f => Bool -> f () -> f ()
На мой взгляд, код ниже более ясно описывает поведение программы, чем две предикатных конструкции ветвления подряд:
debug = True
m :: Int -> IO ()
m i = do
when debug $ putStrLn ("Debugging: " ++ show i)
unless (i > 5) $ m (succ i)
Запуск монадического действия (любая монада - это аппликативный функтор):
> m 3
Debugging: 3
Debugging: 4
Debugging: 5
Debugging: 6