Гвардии против if-then-else против случаев в Haskell

У меня есть три функции, которые находят 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 и случаями?

Ответ 1

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

Как говорится, мое правило для стилей заключается в том, что если вы можете прочитать его, как если бы он был английским (читайте | как "когда", | 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

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

-- 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)

Ответ 2

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

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)

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