Охранники против if-then-else против дел в Haskell

105

У меня есть три функции, которые находят n-й элемент списка:

nthElement :: [a] -> Int -> Maybe a 
nthElement [] a = Nothing
nthElement (x:xs) a | a <= 0 = Nothing
                    | a == 1 = Just x
                    | a > 1 = nthElement xs (a-1)

nthElementIf :: [a] -> Int -> Maybe a
nthElementIf [] a = Nothing
nthElementIf (x:xs) a = if a <= 1
                        then if a <= 0 
                             then Nothing
                             else Just x -- a == 1
                        else nthElementIf xs (a-1)                           

nthElementCases :: [a] -> Int -> Maybe a
nthElementCases [] a = Nothing
nthElementCases (x:xs) a = case a <= 0 of
                             True -> Nothing
                             False -> case a == 1 of
                                        True -> Just x
                                        False -> nthElementCases xs (a-1)

На мой взгляд, первая функция - лучшая реализация, потому что она наиболее лаконична. Но есть ли что-нибудь в двух других реализациях, что могло бы сделать их предпочтительнее? И, в более широком смысле, как бы вы выбрали между использованием охранников, операторов if-then-else и случаев?

ядерный прилив
источник
5
вы можете свернуть свои вложенные caseоператоры, если использовалиcase compare a 0 of LT -> ... | EQ -> ... | GT -> ...
rampion
5
@rampion: вы имеете в видуcase compare a 1 of ...
newacct

Ответы:

122

С технической точки зрения все три версии равнозначны.

При этом мое практическое правило для стилей заключается в том, что если вы можете читать его так, как если бы он был английским (читается |как «когда», | otherwiseкак «иначе» и =как «есть» или «быть»), вы, вероятно, что-то делаете. право.

if..then..elseкогда у вас есть одно двоичное условие или одно единственное решение, которое вам нужно принять. Вложенные if..then..else-выражения очень редко встречаются в Haskell, и вместо них почти всегда следует использовать охранники.

let absOfN =
  if n < 0 -- Single binary expression
  then -n
  else  n

Каждое if..then..elseвыражение может быть заменено защитным элементом, если оно находится на верхнем уровне функции, и это, как правило, предпочтительнее, так как тогда вы можете легко добавить больше вариантов:

abs n
  | n < 0     = -n
  | otherwise =  n

case..ofпредназначен для случаев, когда у вас есть несколько путей кода , и каждый путь кода управляется структурой значения, то есть через сопоставление с образцом. Вы очень редко сопоставляете Trueи False.

case mapping of
  Constant v -> const v
  Function f -> map f

Стражи дополняют case..ofвыражения, что означает, что если вам нужно принимать сложные решения в зависимости от значения, сначала принимайте решения в зависимости от структуры вашего ввода, а затем принимайте решения по значениям в структуре.

handle  ExitSuccess = return ()
handle (ExitFailure code)
  | code < 0  = putStrLn . ("internal error " ++) . show . abs $ code
  | otherwise = putStrLn . ("user error " ++)     . show       $ code

Кстати. В качестве совета по стилю всегда делайте новую строку после =или до, |если материал после =/ |слишком длинный для одной строки или использует больше строк по какой-либо другой причине:

-- NO!
nthElement (x:xs) a | a <= 0 = Nothing
                    | a == 1 = Just x
                    | a > 1 = nthElement xs (a-1)

-- Much more compact! Look at those spaces we didn't waste!
nthElement (x:xs) a
  | a <= 0    = Nothing
  | a == 1    = Just x
  | otherwise = nthElement xs (a-1)
dflemstr
источник
1
«Вы очень редко соответствуете» Trueи Falseесть ли вообще случай, когда вы бы это сделали? В конце концов, такое решение всегда можно принять с помощью ifохранников.
leftaround примерно
2
Egcase (foo, bar, baz) of (True, False, False) -> ...
dflemstr
@dflemstr Нет ли более тонких различий, например, охранников, требующих MonadPlus и возвращающих экземпляр монады, в то время как if-then-else нет? Но я не уверен.
J Fritsch
2
@JFritsch: guardфункция требует MonadPlus, но то, о чем мы говорим здесь, это охранники, как в | test =пунктах, которые не связаны.
Ben Millwood
Спасибо за подсказку по стилю, теперь это подтверждено сомнениями.
eigenfield 01
22

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

nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)
Даниэль Вагнер
источник
3

Это всего лишь вопрос порядка, но я думаю, что он очень удобочитаемый и имеет ту же структуру, что и охранники.

nthElement :: [a] -> Int -> Maybe a 
nthElement [] a = Nothing
nthElement (x:xs) a = if a  < 1 then Nothing else
                      if a == 1 then Just x
                      else nthElement xs (a-1)

Последнее else не нужно, и если, поскольку нет других возможностей, функции также должны иметь «крайний случай» на случай, если вы что-то пропустили.

Кристиан Гарсия
источник
4
Вложенные операторы if являются анти-шаблоном, когда вы можете использовать средства защиты случаев.
user76284