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?


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)

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)

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ł.


4
Zagnieżdżone instrukcje if są anty-wzorcem, kiedy można używać funkcji ochrony wielkości liter.
— user76284
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.