Охранники против 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)
на мой взгляд, первая функция является лучшей реализацией, потому что он является наиболее лаконичным. Но есть ли что-нибудь о двух других реализациях, что сделало бы их предпочтительнее? И, кроме того, как бы вы выбрали между использованием guards, if-then-else и cases?
3 ответа:
С технической точки зрения, все три варианта эквивалентны.
как говорится, мое эмпирическое правило для стилей заключается в том, что если вы можете прочитать его, как если бы он был английским (читать
|
"когда",| 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)
Я знаю, что это вопрос о стиле Для явно рекурсивных функций, но я бы предположил, что лучший стиль-это поиск способа повторного использования существующих рекурсивных функций.
nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)
Это просто вопрос порядка, но я думаю, что его очень читаемый и имеет ту же структуру, что и охранники.
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)
последнее еще не нужно, и если нет никаких других возможностей, также функции должны иметь "крайний случай" в случае, если вы что-то пропустили.