Ответ 1
Самое простое, наиболее очевидное решение
Если вы используете GHC, вы можете включить
{-# LANGUAGE MultiWayIf #-}
и все ваше дело становится
f x = if | c1 x && c2 x -> a x
| c1 x && c3 x && c4 x -> b x
| otherwise -> d x
Немного более продвинутое и гибкое решение
Однако, вы не всегда хотите слепо повторить императивный код в Haskell. Часто полезно придумать свой код как данные. Фактически вы выполняете настройку списка требований, которые должен удовлетворять x
, а затем, если x
удовлетворяет этим требованиям, вы предпринимаете некоторые действия на x
.
Мы можем представить это с помощью фактических списков функций в Haskell. Это выглядело бы как
decisions :: [([a -> Bool], a -> b)]
decisions = [([c1, c2], a)
,([c1, c3, c4], b)]
,([], d)]
Здесь мы должны прочитать это как: "если x
удовлетворяет как c1
, так и c2
, выполните действие a
на x
" и так далее. Тогда мы можем определить f
как
f x = let maybeMatch = find (all ($ x) . fst) decisions
match = fromMaybe (error "no match!") maybeMatch
result = snd match
in result x
Это работает, пройдя список требований и найдя первый набор решений, удовлетворяющих x
(maybeMatch
). Он вытащил это из Maybe
(вам может понадобиться исправление ошибок!) Затем он выбирает соответствующую функцию (result
), а затем выполняет x
через это.
Очень продвинутое и гибкое решение
Если у вас действительно сложное дерево решений, вы можете не захотеть представить его с помощью плоского списка. Это то, где реальные деревья данных пригождаются. Вы можете создать дерево необходимых функций, а затем искать это дерево, пока не нажмете лист node. Это дерево может в этом примере выглядеть примерно как
+-> c1 +-> c2 -> a
| |
| +-> c3 -> c4 -> b
+-> d
Другими словами, если x
удовлетворяет c1
, он увидит, удовлетворяет ли оно также c2
, и если он принимает действие a
на x
. Если это не так, оно переходит к следующей ветки с c3
и т.д., Пока не достигнет действия (или прошло через все дерево).
Но сначала вам понадобится тип данных, чтобы рассказать разницу между требованием (c1
, c2
и т.д.) и действием (a
, b
и т.д.)
data Decision a b = Requirement (a -> Bool)
| Action (a -> b)
Затем вы создаете дерево решений как
decisions =
Node (Requirement (const True))
[Node (Requirement c1)
[Node (Requirement c2)
[Node (Action a) []]
,Node (Requirement c3)
[Node (Requirement c4)
[Node (Action b) []]]
,Node (Action d) []]
Это выглядит сложнее, чем есть, поэтому вы, вероятно, должны придумать более аккуратный способ выражения деревьев решений. Если вы определяете функции
iff = Node . Requirement
action = flip Node [] . Action
вы можете написать дерево как
decisions =
iff (const True) [
iff (c1) [
iff (c2) [
action a
],
iff (c3) [
iff (c4) [
action b
]
]
],
action d
]
и внезапно он очень похож на императивный код, с которого вы начали, несмотря на то, что он действительный код Haskell, который просто строит структуру данных! Haskell является мощным средством для определения пользовательских небольших "языков внутри языка", подобных этому.
Затем вам нужно выполнить поиск по дереву для первого действия, которое вы можете достичь.
decide :: a -> Tree (Decision a b) -> Maybe b
decide x (Node (Action f) _) = Just (f x)
decide x (Node (Requirement p) subtree)
| p x = asum $ map (decide x) subtree
| otherwise = Nothing
Это использует немного магии Maybe (asum
), чтобы остановиться при первом успешном ударе. Это, в свою очередь, означает, что он не будет вычислять условия любой ветки напрасно (что является эффективным и важным, если вычисления дороги), и он должен обрабатывать бесконечные деревья решений только отлично.
Вы можете сделать decide
еще более общим, в полной мере используя класс Alternative
, но я решил специализировать его на Maybe
, чтобы не писать книгу об этом. Сделать его еще более общим может позволить вам иметь причудливые монадические решения, что было бы очень круто!
Но, наконец, как очень простой пример этого в действии - возьмите гипотезу Collatz. Если вы дадите мне номер и спросите меня, каким должен быть следующий номер, я могу построить дерево решений, чтобы узнать. Дерево может выглядеть так:
collatz =
iff (> 0) [
iff (not . even) [
action (\n -> 3*n + 1)
],
action (`div` 2)
]
поэтому число должно быть больше 0, а затем, если оно нечетно, вы умножаетесь на три и добавляете один, иначе вы его уменьшите вдвое. Тесты показывают, что
λ> decide 3 collatz
Just 10
λ> decide 10 collatz
Just 5
λ> decide (-4) collatz
Nothing
Возможно, вы можете представить гораздо более интересные деревья решений.
Редактируйте как год спустя: обобщение на Alternative на самом деле очень простое и довольно интересное. Функция decide
получает новый внешний вид
decide :: Alternative f => a -> Tree (Decision a b) -> f b
decide x (Node (Action f) _) = pure (f x)
decide x (Node (Requirement p) subtree)
| p x = asum $ map (decide x) subtree
| otherwise = empty
(это всего три изменения, для тех, у кого есть счет). То, что это дает вам, - это возможность собрать "все" действия, которые удовлетворяет вход, используя аппликативный экземпляр списков вместо Maybe. Это показывает "ошибку" в нашем дереве collatz
- если мы внимательно посмотрим на это, мы увидим, что все нечетные и положительные целые числа n
обращаются к 3*n +1
, но также говорят, что все положительные числа обращаются к n/2
. Дополнительного требования о том, что число должно быть четным, не существует.
Другими словами, действие (`div` 2)
выполняется только по требованию (>0)
и больше ничего. Это технически некорректно, но это работает, если мы просто получим первый результат (что в основном используется с помощью альтернативного экземпляра Maybe
). Если мы перечислим все результаты, мы также получим неверный.
Когда получается несколько интересных результатов? Возможно, мы пишем дерево решений для ИИ, и мы хотим гуманизировать поведение, сначала получив все правильные решения, а затем выберем один из них наугад. Или ранжируйте их на основе того, насколько хороши они в данных обстоятельствах или что-то еще.