Guardas vs. if-then-else vs. casos em Haskell
Tenho três funções que encontram o elemento n º de uma lista:
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)
Na minha opinião, a primeira função é a melhor implementação, porque é a mais concisa. Mas há alguma coisa sobre as outras duas implementações que os tornaria preferíveis? E por extensão, como você escolheria entre usar guardas, se-então-outras declarações, e casos?
3 answers
Dito isto, a minha regra de ouro para styles é que se você pode lê-lo como se fosse Inglês (Leia |
como "quando", | otherwise
como "caso contrário" e =
Como "é" ou "ser"), você provavelmente está fazendo algo certo.
if..then..else
é para quando você tem uma condição binária, ou uma única decisão que você precisa tomar. As expressões aninhadas if..then..else
são muito raras em Haskell, e os guardas devem quase sempre em vez disso, ser usado.
let absOfN =
if n < 0 -- Single binary expression
then -n
else n
Cada expressão if..then..else
pode ser substituída por uma guarda se estiver no nível superior de uma função, e esta deve ser geralmente preferida, uma vez que pode adicionar mais casos mais facilmente então:
abs n
| n < 0 = -n
| otherwise = n
case..of
é para quando você tem múltiplos caminhos de código, e cada caminho de código é guiado pelo
estrutura de um valor, isto é, através da correspondência de padrões. Raramente se compara a True
e False
.
case mapping of
Constant v -> const v
Function f -> map f
Guardas complementares case..of
expressões, o que significa que se você precisar fazer complicadas decisões dependendo do valor, primeiro tomar decisões, dependendo da estrutura de sua entrada, e então tomar decisões sobre os valores na estrutura.
handle ExitSuccess = return ()
handle (ExitFailure code)
| code < 0 = putStrLn . ("internal error " ++) . show . abs $ code
| otherwise = putStrLn . ("user error " ++) . show $ code
Já agora. como uma dica de estilo, faça sempre uma nova linha após um =
ou antes de um |
Se o material após o =
/|
é muito longo para uma linha, ou usa mais linhas por alguma outra razão:
-- 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)
Eu sei que isto é uma pergunta sobre estilo para funções explicitamente recursivas, mas eu sugeriria que o melhor estilo é encontrar uma maneira de reutilizar as funções recursivas existentes em vez disso.
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)
{[[2]} o último não precisa e se já que não há outras possibilidades, as funções também devem ter" último caso" no caso de você ter perdido alguma coisa.