2009-03-03 11 views
54

Dio odio il termine "odore di codice", ma non riesco a pensare a nulla di più preciso.L'uso di Haskell monade un odore di codice?

Sto progettando un compilatore di lingua di alto livello & nel Whitespace nel mio tempo libero per informazioni sulla costruzione del compilatore, sulla progettazione del linguaggio e sulla programmazione funzionale (il compilatore viene scritto in Haskell).

Durante la fase di generazione del codice del compilatore, devo mantenere i dati "state" -ish mentre percorro l'albero della sintassi. Ad esempio, quando si compila le dichiarazioni di flusso di controllo ho bisogno di generare nomi unici per le etichette di saltare a (etichette generate da un contatore che è passato in, aggiornato, & restituito, e il vecchio valore del contatore non deve mai essere usato di nuovo). Un altro esempio è quando mi imbatto in letterali di stringa in linea nell'albero della sintassi, devono essere convertiti in modo permanente in variabili heap (in Whitespace, le stringhe vengono memorizzate al meglio nell'heap). Sto attualmente avvolgendo l'intero modulo di generazione del codice nella monade di stato per gestirlo.

Mi è stato detto che scrivere un compilatore è un problema adatto al paradigma funzionale, ma trovo che lo sto progettando più o meno nello stesso modo in cui lo progetterei in C (puoi davvero scrivere C in qualsiasi lingua - anche Haskell w/state monads).

Voglio imparare come pensare in Haskell (piuttosto, nel paradigma funzionale) - non in C con la sintassi di Haskell. Dovrei davvero cercare di eliminare/ridurre al minimo l'uso della monade di stato, o è un "modello di progettazione" legittimo e funzionale?

+11

Wtf man, spazio vuoto ... –

+9

Dovrei scrivere un compilatore per mips o x86 asm? Sarebbe un po 'più complicato. – Cybis

+22

C++, Cybis. Le specifiche del 1999 E lo vogliamo venerdì. –

risposta

40

Direi che lo stato in generale non è un odore di codice, a patto che sia tenuto piccolo e ben controllato.

Ciò significa che l'utilizzo di monade come State, ST o personalizzate, o semplicemente avere una struttura dati contenente dati di stato che si passano in pochi punti, non è una cosa negativa.(In realtà, le monadi sono solo di aiuto nel fare esattamente questo!) Tuttavia, avendo uno stato che va dappertutto (sì, questo significa che, IO monade!) È un cattivo odore.

Un esempio abbastanza chiaro di questo è stato quando il mio team stava lavorando al nostro ingresso per lo ICFP Programming Contest 2009 (il codice è disponibile su git: //git.cynic.net/haskell/icfp-contest-2009). Abbiamo finito con parecchie parti modulari differenti a questo:

  • VM: la macchina virtuale che ha eseguito il programma di simulazione
  • Controllers: diverse serie di routine che leggono l'uscita del simulatore e generato nuovi ingressi di controllo
  • Soluzione: generazione del file di soluzione in base all'output dei controller
  • Visualizzatori: diversi set di routine che leggono sia le porte di input che di output e generato una sorta di visualizzazione o registro di ciò che stava accadendo come simulazione progresso

Ognuno di questi ha il proprio stato e tutti interagiscono in vari modi attraverso i valori di input e output della VM. Avevamo diversi controller e visualizzatori, ognuno dei quali aveva il suo diverso tipo di stato.

Il punto chiave qui era che gli interni di un particolare stato erano limitati ai loro moduli particolari, e ogni modulo non sapeva nulla nemmeno dell'esistenza dello stato per altri moduli. Qualsiasi insieme particolare di codice e dati stateful era in genere lungo solo alcune dozzine di righe, con una manciata di elementi di dati nello stato.

Tutto questo era incollato insieme in una piccola funzione di circa una dozzina di linee che non avevano accesso agli interni di nessuno degli stati, e che semplicemente chiamava le cose giuste nell'ordine corretto mentre passava attraverso la simulazione, e passato un numero molto limitato di informazioni esterne a ciascun modulo (insieme allo stato precedente del modulo, ovviamente).

Quando lo stato viene utilizzato in modo limitato e il sistema di tipi impedisce di modificarlo inavvertitamente, è piuttosto semplice da gestire. È una delle bellezze di Haskell che ti consente di farlo.

Una risposta dice "Non usare le monadi". Dal mio punto di vista, questo è esattamente al contrario. Le Monade sono una struttura di controllo che, tra le altre cose, può aiutare a ridurre al minimo la quantità di codice che tocchi stato. Se si considerano i parser monadici come esempio, lo stato dell'analisi (cioè, il testo che viene analizzato, il punto in cui ci si è immessi, eventuali avvisi accumulati, ecc.) Deve essere eseguito attraverso ogni combinatore utilizzato nel parser . Eppure ci saranno solo alcuni combinatori che manipolano direttamente lo stato; qualsiasi altra cosa usa una di queste poche funzioni. Questo ti consente di vedere chiaramente e in un unico punto tutta una piccola quantità di codice che può cambiare lo stato, e più facilmente ragionare su come può essere cambiato, rendendolo ancora più facile da gestire.

+0

Risposta molto bella, anche se non sono sicuro che sia giusto cambiare la risposta "accettata" due mesi dopo. Molte grazie a prescindere. – Cybis

+0

Ho aggiunto un nuovo paragrafo per mostrare perché penso che la risposta accettata sia, su ulteriore riflessione, in realtà la risposta sbagliata. Penso che dovresti cambiarlo; Sono sempre infastidito dalle risposte sbagliate che si trovano in cima alla lista. –

+0

Ho accettato in base alla carta a cui si collegava, non alla risposta stessa (sono d'accordo che la risposta da sola era piuttosto scarsa). Ora che ci ho ripensato, probabilmente non era un buon motivo. L'uso corretto delle monadi riguarda sia la modularità (la tua risposta) sia l'astrazione (la risposta di Norman Ramsey). Se accetto questo, entrambi saranno al top. Sarebbe meglio. – Cybis

-5

Beh, non utilizzare monadi. Il potere della programmazione funzionale è la purezza della funzione e il loro riutilizzo. C'è questo articolo che un mio professore ha scritto una volta ed è uno dei ragazzi che ha aiutato a costruire Haskell.

La carta si chiama "Why functional programming matters", suggerisco di leggerlo. È una buona lettura

+2

Pensavo che l'uso eccessivo delle monadi fosse un odore di codice. Ci sono molti tutorial Haskell online, ma molto poco sulla buona programmazione di Haskell. Mille grazie per il giornale. – Cybis

+16

Solo la monade IO è in qualche modo impura. –

+2

Si noti che John Huges, l'autore di "Perché argomenti di programmazione funzionale", è anche l'autore di "Generalizzare le Monade in frecce", che è anche un modo di gestire lo stato. –

-12

cerchiamo di essere attenti sulla terminologia qui. Lo stato non è di per sé cattivo; le lingue funzionali hanno lo stato. Ciò che è un "odore di codice" è quando ti ritrovi a voler assegnare valori di variabile e modificarli.

Naturalmente, la monade di stato Haskell è lì solo per questo motivo - come con I/O, ti consente di fare cose non sicure e non funzionali in un contesto limitato.

Quindi, sì, probabilmente è un odore di codice.

+16

Perché questo mito viene ripetuto? La maggior parte delle monadi tradizionali non sono né "sicure" né "non funzionali". Anche IO, che l'implementazione/implementazione/è in realtà non funzionale, in quanto è gestito specificamente dal compilatore, potrebbe essere considerato tale. – squadette

+0

Beh, principalmente perché, secondo la definizione, è vero. Anche nel compilatore più sofisticato, il componente I/O deve, per necessità, avere effetti collaterali: lo stato del dispositivo sottostante deve cambiare, oppure non si è verificato alcun I/O. –

+8

L'IO monad (e IIRC il monade ST) è internamente non funzionale ai fini delle prestazioni. Tuttavia, non deve essere.L'ambiente runtime (codice C) può semplicemente eseguire la monade senza che il codice Haskell faccia mai qualcosa di non sicuro o non funzionante. Tutte le altre monadi non sono affatto pericolose o non funzionanti. – Zifre

6

Hai guardato Attribute grammars (AG)? (Maggiori informazioni su wikipedia e un article in Monad Reader)?

Con AG è possibile aggiungere attributi a un albero di sintassi. Questi attributi sono separati in sintetizzato e ereditato attributi.

attributi sintetizzati sono cose che si generano (o sintetizzare) dal vostro albero di sintassi, questo potrebbe essere il codice generato, o tutti i commenti, o qualsiasi altra cosa il vostro interesse.

attributi ereditati sono input per il vostro albero di sintassi, questo potrebbe essere l'ambiente o un elenco di etichette da utilizzare durante la generazione del codice.

Presso l'Università di Utrecht utilizziamo lo Attribute Grammar System (UUAGC) per scrivere i compilatori.Si tratta di un pre-processore che genera codice haskell (file .hs) dai file .ag forniti.


Anche se, se si sta ancora imparando Haskell, allora forse questo non è il momento di iniziare ad imparare un altro livello di astrazione più di questo.

In questo caso, si potrebbe scrivere manualmente il tipo di codice che attribuisce grammatiche generano per voi, per esempio:

data AbstractSyntax = Literal Int | Block AbstractSyntax 
        | Comment String AbstractSyntax 

compile :: AbstractSyntax -> [Label] -> (Code, Comments) 
compile (Literal x) _  = (generateCode x, []) 
compile (Block ast) (l:ls) = let (code', comments) = compile ast ls 
          in (labelCode l code', comments) 
compile (Comment s ast) ls = let (code, comments') = compile ast ls 
          in (code, s : comments') 

generateCode :: Int -> Code 
labelCode :: Label -> Code -> Code 
+0

Certamente qualcosa da tenere a mente, grazie. La mia lingua però non è poi così complessa - è molto simile a lisp ma senza macro, elenchi o funzioni di ordine superiore (questi sarebbero difficili da tradurre in Whitespace). Usare le grammatiche degli attributi potrebbe essere un po 'eccessivo. – Cybis

+0

In tal caso si può sicuramente usare il modello AG manuale. Eliminerebbe la necessità della monade di Stato. –

43

ho scritto più compilatori in Haskell, e una monade stato è una soluzione ragionevole a molti problemi del compilatore. Ma tu vuoi tenerlo astratto --- non rendere evidente che stai usando una monade.

Ecco un esempio del Compilatore Haskell di Glasgow (che ho fatto non in scrittura, ho appena risolto alcuni bordi), dove costruiamo i grafici del flusso di controllo. Qui ci sono i modi principali per i grafici:

empyGraph :: Graph 
mkLabel  :: Label -> Graph 
mkAssignment :: Assignment -> Graph -- modify a register or memory 
mkTransfer :: ControlTransfer -> Graph -- any control transfer 
(<*>)  :: Graph -> Graph -> Graph 

Ma, come hai scoperto, il mantenimento di una fornitura di etichette uniche è noioso nel migliore dei casi, in modo da fornire queste funzioni così:

withFreshLabel :: (Label -> Graph) -> Graph 
mkIfThenElse :: (Label -> Label -> Graph) -- branch condition 
      -> Graph -- code in the 'then' branch 
      -> Graph -- code in the 'else' branch 
      -> Graph -- resulting if-then-else construct 

Il L'intera cosa Graph è un tipo astratto, e il traduttore costruisce animatamente i grafici in modo puramente funzionale, senza essere consapevole del fatto che sta accadendo qualcosa di monadico. Quindi, quando il grafico è finalmente costruito, per trasformarlo in un tipo di dati algebrico da cui possiamo generare il codice, gli diamo una scorta di etichette univoche, eseguiamo la monade di stato e tiriamo fuori la struttura dei dati.

La monade di stato è nascosta sotto; anche se non è esposto al cliente, la definizione di Graph è qualcosa di simile:

type Graph = RealGraph -> [Label] -> (RealGraph, [Label]) 

o un po 'più accuratamente

type Graph = RealGraph -> State [Label] RealGraph 
    -- a Graph is a monadic function from a successor RealGraph to a new RealGraph 

Con la monade stato nascosto dietro uno strato di astrazione, non è maleodorante affatto!

0

In generale dovresti cercare di evitare lo stato ogni volta che è possibile, ma questo non è sempre pratico. Applicative rende il codice effetto più gradevole e più funzionale, in particolare il codice di attraversamento degli alberi può trarre vantaggio da questo stile. Per il problema della generazione dei nomi ora è disponibile un pacchetto piuttosto interessante: value-supply.

2

Non credo che l'uso della Monade di stato sia un odore di codice quando è utilizzato per modellare lo stato.

Se è necessario specificare lo stato tramite le proprie funzioni, è possibile eseguire esplicitamente lo , prendendo lo stato come argomento e restituendolo in ogni funzione. La Monade di stato offre una buona astrazione: passa lo stato per te e offre molte funzioni utili per combinare funzioni che richiedono uno stato. In questo caso, l'utilizzo della Monade di stato (o dei candidati) non è un odore di codice.

Tuttavia, se si utilizza la Monade di stato per emulare uno stile imperativo di programmazione mentre una soluzione funzionale sarebbe sufficiente, si stanno solo complicando le cose.

Problemi correlati