Подтвердить что ты не робот

В структурах управляющих потоков в Haskell (несколько if-then-else)

Я хочу перевести следующую процедурную программу в Haskell [написанную в псевдокоде]:

f(x) {
  if(c1(x)) {
     if(c2(x)) {
        return a(x);
     }
     else if (c3(x)) {
      if(c4(x)) {
         return b(x);
     }
  }
  return d(x);
}

Я написал следующую реализацию:

f x = 
  if (c1 x) then
     if(c2 x) then 
            a x
     else if (c3 x) then
              if (c4 x) then
                  b x
              else d x
     else d x
  else d x 

К сожалению, он содержит (еще d x) три раза.

Есть ли лучший способ реализовать эту функцию? (т.е. вернуть (d x), если ни одно из условий не было выполнено?)

Я понимаю, что мы могли бы комбинировать условия c1 и c2 в (c1 x) && (c2 x), чтобы число, меньшее, но мои условия c1, c2, c3, c4 действительно очень длинные, и если я их объединю, я получу условие, которое занимает более одной строки.

4b9b3361

Ответ 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). Если мы перечислим все результаты, мы также получим неверный.

Когда получается несколько интересных результатов? Возможно, мы пишем дерево решений для ИИ, и мы хотим гуманизировать поведение, сначала получив все правильные решения, а затем выберем один из них наугад. Или ранжируйте их на основе того, насколько хороши они в данных обстоятельствах или что-то еще.

Ответ 2

Вы можете использовать защитные устройства и предложение where:

f x | cb && c2 x = a x
    | cb && c3 x && c4 x = b x
    | otherwise = d x
  where cb = c1 x

Ответ 3

Если вы просто беспокоитесь о том, чтобы написать их, то, что where блоки для

f x = 
  case () of
    () | c1 && c2       -> a x
       | c1 && c3 && c4 -> b x
       | otherwise      -> d x
  where
    c1 = ...
    c2 = ...
    c3 = ...
    c4 = ...

Не то, чтобы я использовал трюк case, чтобы ввести новое место для инструкций охраны. Я не могу использовать защитные устройства для самого определения функции, потому что предложение where не будет охватывать все защитные устройства. Вы можете использовать if все то же самое, но у охранников есть хорошая сквозная семантика.

Ответ 4

Вот еще один шаблон, который вы можете использовать: я бы не использовал его в вашем конкретном примере, но есть очень похожие ситуации, в которых я его использовал.

f x = case (c1 x, c2 x, c3 x, c4 x) of
  (True,True,_,_) -> a x
  (True,False,True,True) -> b x
  _ -> d x

На самом деле будет оценена только минимальная оценка, необходимая для выбора того, какой путь принять: на самом деле она не будет оценивать c2 x, если c1 x не является True.