Gardes vs si-alors-autre vs cas à Haskell

104

J'ai trois fonctions qui trouvent le nième élément d'une liste:

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)

À mon avis, la première fonction est la meilleure implémentation car elle est la plus concise. Mais y a-t-il quelque chose dans les deux autres implémentations qui les rendrait préférables? Et par extension, comment choisiriez-vous entre l'utilisation de gardes, des déclarations if-then-else et des cas?

marée nucléaire
la source
5
vous pouvez casecase compare a 0 of LT -> ... | EQ -> ... | GT -> ...
réduire
5
@rampion: you meancase compare a 1 of ...
newacct

Réponses:

122

D'un point de vue technique, les trois versions sont équivalentes.

Cela étant dit, ma règle de base pour les styles est que si vous pouvez le lire comme s'il était anglais (lire |comme "quand", | otherwisecomme "autrement" et =comme "est" ou "être"), vous faites probablement quelque chose droite.

if..then..elseest pour quand vous avez une condition binaire , ou une seule décision que vous devez prendre. Les if..then..elseexpressions imbriquées sont très rares dans Haskell, et les gardes devraient presque toujours être utilisées à la place.

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

Chaque if..then..elseexpression peut être remplacée par une garde si elle est au niveau supérieur d'une fonction, et cela devrait généralement être préféré, car vous pouvez ajouter plus de cas plus facilement alors:

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

case..ofest pour lorsque vous avez plusieurs chemins de code , et chaque chemin de code est guidé par la structure d'une valeur, c'est-à-dire via la correspondance de modèles. Vous correspondez très rarement sur Trueet False.

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

Les gardes complètent les case..ofexpressions, ce qui signifie que si vous devez prendre des décisions compliquées en fonction d'une valeur, prenez d' abord des décisions en fonction de la structure de votre entrée, puis prenez des décisions sur les valeurs de la structure.

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

BTW. Comme astuce de style, faites toujours une nouvelle ligne après a =ou avant a |si le truc après le =/ |est trop long pour une ligne, ou utilise plus de lignes pour une autre raison:

-- 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
la source
1
"Vous correspondez très rarement Trueet False" y a-t-il une occasion où vous feriez ça? Après tout, ce genre de décision peut toujours être fait avec un if, et avec des gardes aussi.
gauche vers le
2
Par exemplecase (foo, bar, baz) of (True, False, False) -> ...
dflemstr
@dflemstr N'y a-t-il pas plus de différences subtiles, par exemple les gardes nécessitant MonadPlus et et retournent une instance de monade alors que if-then-else ne le fait pas? Mais je ne suis pas sûr.
J Fritsch
2
@JFritsch: la guardfonction nécessite MonadPlus, mais ce dont nous parlons ici, ce sont des gardes comme dans des | test =clauses, qui ne sont pas liées.
Ben Millwood
Merci pour le conseil de style, maintenant confirmé par le doute.
truthadjustr
22

Je sais que c'est une question de style pour les fonctions explicitement récursives, mais je suggérerais que le meilleur style est de trouver un moyen de réutiliser les fonctions récursives existantes à la place.

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

C'est juste une question de commande mais je pense que c'est très lisible et a la même structure que les gardes.

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)

Le dernier autre n'a pas besoin et s'il n'y a pas d'autres possibilités, les fonctions devraient également avoir un "cas de dernier recours" au cas où vous auriez manqué quelque chose.

Cristian Garcia
la source
4
Les instructions if imbriquées sont un anti-modèle lorsque vous pouvez utiliser des protections de cas.
user76284