Strażnicy a jeśli-to-inaczej a przypadki w Haskell

105

Mam trzy funkcje, które znajdują n-ty element listy:

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)

Moim zdaniem pierwsza funkcja jest najlepszą implementacją, ponieważ jest najbardziej zwięzła. Ale czy jest coś w pozostałych dwóch implementacjach, co sprawiłoby, że byłyby lepsze? A co za tym idzie, jak wybrałbyś między używaniem strażników, stwierdzeniami if-to-else i przypadkami?

nukleartyd
źródło
5
możesz zwinąć swoje zagnieżdżone caseinstrukcje, jeśli użyłeścase compare a 0 of LT -> ... | EQ -> ... | GT -> ...
rampion
5
@rampion: masz na myślicase compare a 1 of ...
newacct

Odpowiedzi:

122

Z technicznego punktu widzenia wszystkie trzy wersje są równoważne.

Biorąc to pod uwagę, moją praktyczną zasadą dotyczącą stylów jest to, że jeśli możesz czytać to tak, jakby był angielski (czytaj |„kiedy”, | otherwisejako „inaczej” i =jako „jest” lub „być”), prawdopodobnie robisz coś dobrze.

if..then..elsedotyczy sytuacji, gdy masz jeden warunek binarny lub jedną decyzję, którą musisz podjąć. Zagnieżdżone if..then..elsewyrażenia są bardzo rzadkie w Haskell i prawie zawsze należy używać strażników.

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

Każde if..then..elsewyrażenie można zastąpić strażnikiem, jeśli znajduje się na najwyższym poziomie funkcji, i generalnie powinno to być preferowane, ponieważ można wtedy łatwiej dodać więcej przypadków:

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

case..ofjest dla sytuacji, gdy masz wiele ścieżek kodu , a każda ścieżka kodu jest prowadzona przez strukturę wartości, tj. poprzez dopasowywanie wzorców. Bardzo rzadko pasujesz na Truei False.

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

Strażnicy uzupełniają case..ofwyrażenia, co oznacza, że ​​jeśli musisz podejmować skomplikowane decyzje w zależności od wartości, najpierw podejmuj decyzje w zależności od struktury danych wejściowych, a następnie podejmuj decyzje dotyczące wartości w strukturze.

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

BTW. Jako wskazówka stylistyczna, zawsze rób nową linię po =lub przed a, |jeśli element po =/ |jest zbyt długi dla jednej linii lub używa więcej linii z innego powodu:

-- 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
źródło
1
„Bardzo rzadko pasujesz dalej Truei False” czy jest jakaś okazja, by to zrobić? W końcu tego rodzaju decyzje zawsze można podjąć za pomocą if, a także ze strażnikami.
leftaround około
2
Np.case (foo, bar, baz) of (True, False, False) -> ...
dflemstr
@dflemstr Czy nie ma bardziej subtelnych różnic, np. strażników wymagających MonadPlus i zwracających instancję monady, podczas gdy if-then-else nie? Ale nie jestem pewien.
J Fritsch,
2
@JFritsch: guardfunkcja wymaga MonadPlus, ale mówimy tutaj o strażnikach, jak w | test =klauzulach, które nie są powiązane.
Ben Millwood
Dzięki za wskazówkę stylistyczną, teraz potwierdzona wątpliwościami.
eigenfield
22

Wiem, że jest to kwestia stylu dla funkcji jawnie rekurencyjnych, ale sugerowałbym, że najlepszym stylem jest znalezienie sposobu na ponowne użycie istniejących funkcji rekurencyjnych.

nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)
Daniel Wagner
źródło
3

To tylko kwestia zamówienia, ale myślę, że jest bardzo czytelny i ma taką samą strukturę jak osłony.

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)

To ostatnie nie potrzebuje, a jeśli nie ma innych możliwości, również funkcje powinny mieć „przypadek ostatniej szansy” na wypadek, gdybyś coś przeoczył.

Cristian Garcia
źródło
4
Zagnieżdżone instrukcje if są anty-wzorcem, kiedy można używać funkcji ochrony wielkości liter.
user76284