Protetores vs. if-then-else vs. casos em Haskell

104

Tenho três funções que encontram o enésimo elemento 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á algo sobre as outras duas implementações que as tornaria preferíveis? E, por extensão, como você escolheria entre usar guardas, declarações if-then-else e casos?

nucleartídeo
fonte
5
você pode recolher suas caseinstruções aninhadas se você usoucase compare a 0 of LT -> ... | EQ -> ... | GT -> ...
rampion
5
@rampion: você quer dizercase compare a 1 of ...
newacct

Respostas:

121

Do ponto de vista técnico, todas as três versões são equivalentes.

Dito isso, minha regra para estilos é que se você pode ler como se fosse inglês (leia |como "quando", | otherwisecomo "caso contrário" e =como "é" ou "seja"), provavelmente você está fazendo algo certo.

if..then..elseé para quando você tem uma condição binária ou uma única decisão que precisa tomar. if..then..elseExpressões aninhadas são muito incomuns em Haskell, e guardas quase sempre devem ser usados ​​em seu lugar.

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

Cada if..then..elseexpressão pode ser substituída por um guarda se estiver no nível superior de uma função, e isso geralmente deve ser preferido, já que você pode adicionar mais casos mais facilmente do que:

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

case..ofé para quando você tem vários caminhos de código e cada caminho de código é guiado pela estrutura de um valor, ou seja, por meio de correspondência de padrões. Você raramente combina em Truee False.

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

Os guardas complementam as case..ofexpressões, o que significa que se você precisar tomar decisões complicadas dependendo de um valor, primeiro tome decisões dependendo da estrutura de sua entrada e, em seguida, tome 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

BTW. Como dica de estilo, sempre faça uma nova linha depois de a =ou antes de a |se o material depois de =/ |for muito longo para uma linha ou usar mais linhas por algum outro motivo:

-- 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
fonte
1
"Você raramente combina Truee False" há alguma ocasião em que você faria isso? Afinal, esse tipo de decisão sempre pode ser feito com um if, e com os guardas também.
esquerda por volta de
2
Excase (foo, bar, baz) of (True, False, False) -> ...
dflemstr
@dflemstr Não existem diferenças mais sutis, por exemplo, guardas que exigem MonadPlus ee retornam uma instância de mônada, enquanto if-then-else não o faz? Mas eu não tenho certeza.
J Fritsch
2
@JFritsch: a guardfunção requer MonadPlus, mas o que estamos falando aqui são guardas como em | test =cláusulas, que não estão relacionadas.
Ben Millwood
Obrigado pela dica de estilo, agora confirmada por dúvida.
truthadjustr 01 de
22

Eu sei que esta é uma questão sobre o estilo para funções recursivas explicitamente, mas eu sugeriria que o melhor estilo é encontrar uma maneira de reutilizar funções recursivas existentes.

nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)
Daniel Wagner
fonte
2

É só uma questão de ordem, mas acho muito legível e tem a mesma estrutura dos guardas.

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)

O último else não precisa e se não houver outras possibilidades, as funções também devem ter "último recurso" caso você tenha perdido alguma coisa.

Cristian garcia
fonte
4
As instruções if aninhadas são um antipadrão quando você pode usar protetores de caso.
user76284