2009-05-07 4 views
17

Si raccomanda di avere sempre corrispondenze di pattern esaurienti in Haskell, anche per casi "impossibili"?Si raccomanda di avere sempre corrispondenze di pattern esaurienti in Haskell, anche per casi "impossibili"?

Ad esempio, nel seguente codice, I am pattern matching sull'accumulatore di un foldr. Ho il controllo completo del contenuto dell'accumulatore, perché lo creo (non è passato a me come input, ma piuttosto costruito all'interno della mia funzione). Pertanto, so che alcuni modelli non dovrebbero mai combaciare. Se mi sforzerò di non ottenere mai l'errore "Pattern match (es) non-exhaustive", allora inserirò un pattern match che semplicemente l'errore con il messaggio "Questo pattern non dovrebbe mai accadere". Molto simile a un assert in C#. Non riesco a pensare a nient'altro da fare lì.

Quale pratica consiglieresti in questa situazione e perché?

Ecco il codice:

gb_groupBy p input = foldr step [] input 
    where 
     step item acc = case acc of 
      []       -> [[item]] 
      ((x:xs):ys)     -> if p x item 
              then (item:x:xs):ys 
              else [item]:acc 

Il pattern non corrisponde (come riportato dall'interprete) è:

Attenzione: corrispondenza di schema (es) sono non esaustivo In un caso alternativo: Reticoli non abbinato: []: _

+9

La cosa fastidiosa qui è che per silenziare l'avviso si finisce spesso per inserire un messaggio di errore di runtime meno utile. Mi piacerebbe solo riconoscere il caso mancante, ma consentire il testo di errore di runtime predefinito (che punta al numero di file/linea) da utilizzare. –

+0

Questo è un grande punto. Immagino che al momento non ci sia modo di farlo. Peccato. –

+0

vedere anche http://stackoverflow.com/questions/1882334 – sdcvvc

risposta

19

Questa è probabilmente più una questione di stile che altro. Personalmente, vorrei mettere in una

_ -> error "Impossible! Empty list in step" 

se non altro per mettere a tacere l'avviso :)

+1

Sono d'accordo con questo approccio. Aiuta a indicare che non hai gestito intenzionalmente gli altri casi perché ritieni che sia impossibile, piuttosto che ti dimentichi o non ti rendi conto. – newacct

+6

È sempre divertente quando il tuo programma termina improvvisamente con questo messaggio descrittivo: *** Eccezione: l'impossibile è successo! –

+0

Sì, mi ricorda quando ero molto nuovo alla programmazione e avrei commenti come "Non dovresti mai raggiungere questa linea". :) –

10

È possibile risolvere l'avviso in questo caso particolare in questo modo:

gb_groupBy p input = foldr step [] input 
    where 
    step item acc = case acc of 
     []       -> [[item]] 
     (xs:xss)      -> if p (head xs) item 
             then (item:xs):xss 
             else [item]:acc 

Il pattern matching è quindi completare e la condizione "impossibile" di una lista vuota in testa all'accumulatore causerebbe un errore di runtime ma nessun avvertimento.

Un altro modo di considerare il problema più generale delle corrispondenze di modelli incompleti è vederli come un "odore di codice", ovvero un'indicazione che stiamo cercando di risolvere un problema in modo subottimale, o non Haskellish, e prova a riscrivere le nostre funzioni.

Implementare un gruppo Con un foldr è impossibile applicarlo a una lista infinita, che è un obiettivo di progettazione che le funzioni Haskell List cercano di raggiungere ovunque semanticamente ragionevole. Considerare

take 5 $ groupBy (==) someFunctionDerivingAnInfiniteList 

Se i primi 5 gruppi con. l'uguaglianza è finita, la valutazione pigra terminerà. Questo è qualcosa che non puoi fare in una lingua strettamente valutata. Anche se non si lavora con le liste infinite, scrittura di funzioni come questo produrrà una migliore performance su lunghe liste, o evitare l'overflow dello stack che si verifica quando la valutazione di espressioni come

take 5 $ gb_groupBy (==) [1..1000000] 

In List.hs, groupBy è implementata in questo modo:

groupBy   :: (a -> a -> Bool) -> [a] -> [[a]] 
groupBy _ []  = [] 
groupBy eq (x:xs) = (x:ys) : groupBy eq zs 
          where (ys,zs) = span (eq x) xs 

Ciò consente all'interprete/compilatore di valutare solo le parti del calcolo necessarie per il risultato. span produce una coppia di liste, dove il primo consiste di elementi (consecutivi) dal capo della lista che soddisfano tutti un predicato, e il secondo è il resto della lista. È anche implementato per funzionare su liste infinite.

+0

+1, perspicace. –

+0

> Questo è qualcosa che non si può fare in una lingua strettamente valutata Questo può essere fatto in qualsiasi lingua completa di Turing ... E a volte anche non troppo difficile. Ad esempio, è possibile modellare facilmente elenchi "infiniti" anche in Java. –

+1

Il calcolo di Turing si riferisce al tipo di calcolo che è possibile eseguire, non a come lo si specifica, essendo quest'ultimo l'ambito ovvio della mia risposta. Naturalmente puoi mettere insieme qualcosa, ad esempio, con le utility di raccolta di Guava in Java o, più elegantemente, creare qualcosa usando LINQ.NETTO. Ma ciò richiede raccolte che non implementino solo Iterable o IEnumerable, ma abilitino esplicitamente il calcolo lazy, ad es. implementando l'AbstractIterator di Guava o la parola chiave yield in C#. In Haskell, tutto ciò che restituisce una collezione può essere usato pigramente, senza ulteriori indugi. – Christoph

4

Per dare seguito al mio commento precedente, mi sono reso conto che esiste un modo per riconoscere il caso mancante ma ottenere comunque un errore utile con il numero di file/linea. Non è l'ideale visto che apparirà solo in build non ottimizzate, comunque (vedi here).

... 
[]:xs -> assert False (error "unreachable because I know everything") 
+0

Bello. Grazie per il follow-up. –

7

Trovo il controllo esaustivo sui modelli di caso indispensabili. Cerco di non usare mai _ in un caso al livello più alto, perché _ corrisponde a tutto, e usandolo si vince il valore del controllo esaustivo. Questo è meno importante con le liste, ma è di fondamentale importanza con i tipi di dati algebrici definiti dall'utente, perché voglio essere in grado di aggiungere un nuovo costruttore e avere il baril del compilatore su tutti i casi mancanti. Per questo motivo compilo sempre con -Werror acceso, quindi non c'è modo di escludere un caso.

Come osservato, il codice può essere esteso con questo caso

[] : _ -> error "this can't happen" 

Internamente, GHC ha una funzione panic, che a differenza error darà le coordinate di origine, ma ho guardato l'attuazione e non poteva fare di testa o coda di esso.

+2

Mi piace molto come linea guida. Durante la programmazione in C#, ho spesso desiderato qualcosa come "controllo esaustivo". Per esempio, se avessi un enum e una dichiarazione di un caso per gestire ogni membro di enum, volevo essere in grado di dire al compilatore "assicurati di aver coperto tutti i casi". Non avevo idea che un tale concetto sarebbe stato parte delle basi di Haskell e di altri lang funzionali. –

+0

Oh sì, mi sento sempre in colpa quando uso enum e switch. Ma è molto più conciso che creare un'intera gerarchia di classi e suddividere un metodo serio in molti piccoli metodi virtuali. –

+0

Sì. Sono un grande sostenitore del polimorfismo, ma anche a parte questo, ci sono casi in cui si desidera "forgiare" un valore scomposizione in sottocategorie (meno di 3, 3 o superiore ma dispari e 3 o più ma anche). Se lo esprimi in modo errato, hai errori "limite". Ma il compilatore potrebbe facilmente prenderlo per te se sapesse che stai cercando di coprire tutte le possibili gamme. E questa è una delle cose che fa lo schema di corrispondenza in molti dei linguaggi funzionali. –

2

Il sistema di tipi è tuo amico e l'avviso ti informa che la tua funzione presenta delle crepe. L'approccio migliore è quello di trovare una corrispondenza più pulita e più elegante tra i tipi.

consideri la definizione di GHC di groupBy:

groupBy     :: (a -> a -> Bool) -> [a] -> [[a]] 
groupBy _ []   = [] 
groupBy eq (x:xs)  = (x:ys) : groupBy eq zs 
          where (ys,zs) = span (eq x) xs 
1

Il mio punto di vista è che un caso impossibile è definito.
Se è indefinito, abbiamo una funzione: il furbescamente chiamato undefined.

Completate la vostra corrispondenza con artisti del calibro di:

_ -> undefined 

E il gioco è fatto!