Wachen gegen Wenn-Dann-Sonst gegen Fälle in Haskell

104

Ich habe drei Funktionen, die das n-te Element einer Liste finden:

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)

Meiner Meinung nach ist die erste Funktion die beste Implementierung, weil sie am prägnantesten ist. Aber gibt es irgendetwas an den beiden anderen Implementierungen, das sie vorzuziehen wäre? Und im weiteren Sinne, wie würden Sie zwischen der Verwendung von Wachen, Wenn-Dann-Sonst-Anweisungen und Fällen wählen?

Nuklearflut
quelle
5
Sie können Ihre verschachtelten caseAnweisungen case compare a 0 of LT -> ... | EQ -> ... | GT -> ...
reduzieren,
5
@ Rampion: Sie meinencase compare a 1 of ...
Newacct

Antworten:

121

Aus technischer Sicht sind alle drei Versionen gleichwertig.

Abgesehen davon lautet meine Faustregel für Stile, dass Sie wahrscheinlich etwas tun , wenn Sie es so lesen können, als wäre es Englisch (gelesen |als "wann", | otherwiseals "anders" und =als "ist" oder "sein") richtig.

if..then..elseist für den Fall, dass Sie eine binäre Bedingung oder eine einzelne Entscheidung haben, die Sie treffen müssen. Verschachtelte if..then..elseAusdrücke sind in Haskell sehr selten, und stattdessen sollten fast immer Wachen verwendet werden.

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

Jeder if..then..elseAusdruck kann durch einen Schutz ersetzt werden, wenn er sich auf der obersten Ebene einer Funktion befindet. Dies sollte im Allgemeinen bevorzugt werden, da Sie dann einfacher weitere Fälle hinzufügen können:

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

case..ofDies ist der Fall , wenn Sie mehrere Codepfade haben und jeder Codepfad von der Struktur eines Werts geleitet wird, dh über den Mustervergleich. Sie passen sehr selten auf Trueund False.

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

Guards ergänzen case..ofAusdrücke. Wenn Sie also komplizierte Entscheidungen in Abhängigkeit von einem Wert treffen müssen, treffen Sie zuerst Entscheidungen in Abhängigkeit von der Struktur Ihrer Eingabe und dann Entscheidungen über die Werte in der Struktur.

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

Übrigens. Machen Sie als Stiltipp immer eine neue Zeile nach a =oder vor a, |wenn das Material nach dem =/ |für eine Zeile zu lang ist oder aus einem anderen Grund mehr Zeilen verwendet:

-- 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
quelle
1
"Sie passen sehr selten zusammen Trueund False" gibt es überhaupt eine Gelegenheit, wo Sie das tun würden? Schließlich kann diese Art von Entscheidung immer mit einem ifund auch mit Wachen getroffen werden.
links um den
2
ZBcase (foo, bar, baz) of (True, False, False) -> ...
dflemstr
@dflemstr Gibt es keine subtileren Unterschiede, z. B. Wachen, die MonadPlus benötigen und eine Instanz von Monad zurückgeben, während wenn-dann-sonst nicht? Aber ich bin mir nicht sicher.
J Fritsch
2
@JFritsch: Die guardFunktion erfordert MonadPlus, aber wir sprechen hier von Wachen wie in | test =Klauseln, die nicht verwandt sind.
Ben Millwood
Danke für den Style-Tipp, der jetzt durch Zweifel bestätigt wurde.
Truthadjustr
22

Ich weiß, dass dies eine Frage zum Stil für explizit rekursive Funktionen ist, aber ich würde vorschlagen, dass der beste Stil darin besteht, stattdessen vorhandene rekursive Funktionen wiederzuverwenden.

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

Dies ist nur eine Frage der Bestellung, aber ich denke, es ist sehr gut lesbar und hat die gleiche Struktur wie Wachen.

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)

Letzteres braucht man nicht und wenn es keine anderen Möglichkeiten gibt, sollten auch Funktionen "Last-Resort-Fall" haben, falls Sie etwas verpasst haben.

Cristian Garcia
quelle
4
Verschachtelte if-Anweisungen sind ein Anti-Pattern, wenn Sie Case Guards verwenden können.
user76284