Guardie contro se-allora-altro contro casi ad Haskell


104

Ho tre funzioni che trovano l'ennesimo elemento di una 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)

A mio parere, la prima funzione è la migliore implementazione perché è la più concisa. Ma c'è qualcosa nelle altre due implementazioni che le renderebbe preferibili? E per estensione, come sceglieresti tra l'uso di guardie, dichiarazioni if-then-else e casi?


5
puoi comprimere le tue caseistruzioni annidate se hai usatocase compare a 0 of LT -> ... | EQ -> ... | GT -> ...
rampion

5
@ Rampion: vuoi direcase compare a 1 of ...
newacct

Risposte:


121

Da un punto di vista tecnico, tutte e tre le versioni sono equivalenti.

Detto questo, la mia regola pratica per gli stili è che se puoi leggerlo come se fosse inglese (leggi |come "quando", | otherwisecome "altrimenti" e =come "è" o "essere"), probabilmente stai facendo qualcosa giusto.

if..then..elseè per quando hai una condizione binaria o un'unica decisione che devi prendere. Le if..then..elseespressioni annidate sono molto rare in Haskell e dovrebbero essere usate quasi sempre le guardie.

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

Ogni if..then..elseespressione può essere sostituita da una guardia se si trova al livello più alto di una funzione, e questo dovrebbe generalmente essere preferito, poiché puoi aggiungere più casi più facilmente di quanto segue:

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

case..ofè per quando si hanno più percorsi di codice e ogni percorso di codice è guidato dalla struttura di un valore, cioè tramite la corrispondenza del modello. Molto raramente incontri su Truee False.

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

Le protezioni completano le case..ofespressioni, il che significa che se devi prendere decisioni complicate in base a un valore, prendi prima decisioni in base alla struttura del tuo input, quindi prendi decisioni sui valori nella struttura.

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

BTW. Come suggerimento di stile, crea sempre una nuova riga dopo =o prima di a |se il contenuto dopo la =/ |è troppo lungo per una riga o utilizza più righe per qualche altro 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)

1
"Molto raramente incontri Truee False" c'è qualche occasione in cui lo faresti? Dopotutto, questo tipo di decisione può sempre essere presa con un if, e anche con le guardie.
sinistra intorno al

2
Ad esempiocase (foo, bar, baz) of (True, False, False) -> ...
dflemstr,

@dflemstr Non ci sono differenze più sottili, ad esempio guardie che richiedono MonadPlus e restituiscono un'istanza di monade mentre if-then-else no? Ma non sono sicuro.
J Fritsch

2
@ JFritsch: la guardfunzione richiede MonadPlus, ma quello di cui stiamo parlando qui sono guardie come nelle | test =clausole, che non sono correlate.
Ben Millwood

Grazie per il consiglio di stile, ora confermato dal dubbio.
truthadjustr

22

So che questa è una domanda sullo stile per funzioni ricorsive esplicitamente, ma suggerirei che lo stile migliore è trovare un modo per riutilizzare invece le funzioni ricorsive esistenti.

nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)

2

Questa è solo una questione di ordine ma penso che sia molto leggibile e abbia la stessa struttura delle guardie.

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)

L'ultimo altro non serve e se non ci sono altre possibilità, anche le funzioni dovrebbero avere "caso di ultima istanza" nel caso ti sia perso qualcosa.


4
Le istruzioni if ​​annidate sono un anti-pattern quando è possibile utilizzare i case guard.
user76284
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.