2009-11-22 3 views
11

Sono totalmente nuovo in Haskell e quando si scrivono piccoli programmi di solito finiscono con troppe clausole dove controllare molte cose nella funzione, quindi è buona pratica scrivere dove clausole o ci sono altre buone alternative per questo?Haskell troppe clausole where, eventuali suggerimenti alternativi

per esempio nel codice seguente ho provato a trovare se ci sono elementi di formica in ogni riga della lista bidimensionale, funziona e ogni cosa è contenta nella stessa funzione ma non sono soddisfatto di come il codice appaia e io trovare lo stile più imperativo di approccio al problema, quindi sto cercando qualsiasi suggerimento o pensiero su questo da persone con esperienza là fuori.

noDups :: [[a]] -> Bool 
noDups du = and (checkSu du) 
     where 
     checkDup []  = [] 
     checkDup (x:xs) = checkRow x ++ checkDup xs 
      where 
      checkRow []  = [] 
      checkRow (x:xs) = [x /= y | y <- xs] ++ checkRow xs 

ancora una volta questo codice è solo per illustrare un problema, sto cercando un approccio per formulare un problema in stile funzionale. i tuoi suggerimenti o articoli, i collegamenti sarebbero molto utili.

Grazie

risposta

24

provare a scrivere astratte, funzioni riutilizzabili sarete in grado di comporre loro molto più facile

isUnique :: Eq a => [a] -> Bool 
isUnique [] = True 
isUnique (x:xs) = all (/= x) xs && isUnique xs 

noDups :: Eq a => [[a]] -> Bool 
noDups = all isUnique 
+0

+1 per suggerire di dividere in funzioni più piccole invece di provare a destreggiarsi tra la complessità in una volta. – Joren

3
noDups :: [[a]] -> Bool 
noDups = and . checkDup 
    where 
    --checkDup 
    checkDup []  = [] 
    checkDup (x:xs) = checkRow x ++ checkDup xs 
    --alternatively 
    checkDup xs = concat $ map checkRow xs 
    --alternatively 
    checkDup = concat . map checkRow 
    --alternatively 
    checkDup = concatMap checkRow 
    --checkRow 
    checkRow []  = [] 
    checkRow (x:xs) = [x /= y | y <- xs] ++ checkRow xs 
2

Anche se ci sono delle eccezioni, in generale, si consiglia di definire " "funzioni positive", in questo caso definire una funzione che restituisce True se l'argomento fa contiene alcuni dati duplicati. Si potrebbe scrivere che in questo modo:

has_nested_duplicate :: (Eq a) => [[a]] -> Bool 
has_nested_duplicate = any has_duplicate 
    where 
    has_duplicate []  = False 
    has_duplicate (x:xs) = x `elem` xs || has_duplicate xs 

Questo utilizza il pattern matching, any, elem e (||). Per ottenere la negazione, utilizzare not:

noDups :: (Eq a) => [[a]] -> Bool 
noDups = not . has_nested_duplicate 
6

Non è necessario la seconda clausola where. è possibile inserire più funzioni nella stessa clausola where. Tutti i nomi delle funzioni nella stessa clausola where rientrano nei corpi di tali funzioni. Pensa a come funzionano le funzioni di alto livello. Così si potrebbe scrivere:

noDups :: [[a]] -> Bool 
noDups du = and (checkSu du) 
    where checkDup []  = [] 
     checkDup (x:xs) = checkRow x ++ checkDup xs 

     checkRow []  = [] 
     checkRow (x:xs) = [x /= y | y <- xs] ++ checkRow xs 

In realtà questo è molto più chiaro, perché nella vostra versione quando si associa x in checkDup, che x è ancora portata nella secondawhere clausola , eppure si sono vincolanti il argomenti di checkRow con lo stesso nome. Penso che questo probabilmente farà lamentare GHC, ed è certamente fonte di confusione.

4

Lasciando da parte alcuni dei dettagli del vostro esempio particolare (i nomi non sono specialmente ben scelti), io sono un grande fan di clausole Where:

  • una funzione definita in una clausola where può essere meglio di una funzione di primo livello perché un lettore sa che lo scopo della funzione è limitato --- può essere usato in pochi posti.

  • una funzione definita in una clausola where in grado di catturare i parametri di una funzione di inclusione, che spesso rende più facile da leggere

Nel vostro esempio particolare, non è necessario per il nido where clauses- - una singola clausola where, poiché le funzioni definite nella stessa clausola where sono reciprocamente ricorsive l'una con l'altra. Ci sono altre cose sul codice che potrebbero essere migliorate, ma con la singola clausola where mi piace la struttura su larga scala.

N.B. Non è necessario indentare le clausole where così profondamente come si sta facendo.

1

Haskell consente di fare riferimento alle cose definite in una clausola where dalla clausola where (stessa cosa di let binding). In effetti una clausola where è meramente zucchero sintattico per un legame let che consente più definizioni e riferimenti reciproci tra le altre cose.

un esempio è in ordine.

noDups :: [[a]] -> Bool 
noDups du = and (checkDup du) 
    where 
     checkDup []  = [] 
     checkDup (x:xs) = checkRow x ++ checkDup xs 
      where 
       checkRow []  = [] 
       checkRow (x:xs) = [x /= y | y <- xs] ++ checkRow xs 

diventa

noDups :: [[a]] -> Bool 
noDups du = and (checkDup du) 
    where 
     checkDup []  = [] 
     checkDup (x:xs) = checkRow x ++ checkDup xs 
     --checkDup can refer to checkRow 
     checkRow []  = [] 
     checkRow (x:xs) = [x /= y | y <- xs] ++ checkRow xs 

diventa

noDups :: [[a]] -> Bool 
noDups du = 
    let checkDup []  = [] 
     checkDup (x:xs) = checkRow x ++ checkDup xs 
     --checkDup can refer to checkRow 
     checkRow []  = [] 
     checkRow (x:xs) = [x /= y | y <- xs] ++ checkRow xs 

    in and (checkDup du) 
1

Mi sento lo stesso di Norman su come mantenere la portata globale pulita. Più funzioni esponi nello all'interno del modulo , più diventa maldestro lo spazio dei nomi. D'altra parte, avere una funzione nell'ambito globale del tuo modulo lo rende riutilizzabile.

Penso che si possa fare una chiara distinzione. Alcune funzioni sono principali di un modulo, contribuiscono direttamente all'api. Ci sono anche funzioni che quando vengono visualizzate nella documentazione del modulo lascia il lettore chiedendosi che cosa questa particolare funzione ha a che fare con lo scopo del modulo. Questa è chiaramente una funzione di aiuto.

Direi che una tale funzione di supporto dovrebbe essere di prima mano subordinata alla funzione di chiamata. Se questa funzione di supporto deve essere riutilizzata all'interno del modulo, disaccoppiare questa funzione di supporto dalla funzione di chiamata rendendola una funzione accessibile direttamente del modulo. Probabilmente non esporterai questa funzione nella definizione del modulo.
Chiamiamo questo refactoring in stile FP.

È un peccato che non esista un libro "codice completo" -come per la programmazione funzionale. Penso che la ragione sia che c'è troppo poca pratica nel settore. Ma raccogliamo la saggezza sullo stackoverflow: D