Domanda

Dio, odio il termine "odore di codice", ma non riesco a pensare a qualcosa di più accurato.

Sto progettando un linguaggio di alto livello e amp; compilatore a Whitespace nel mio tempo libero per conoscere la costruzione del compilatore, la progettazione del linguaggio e la programmazione funzionale ( il compilatore è stato scritto in Haskell).

Durante la fase di generazione del codice del compilatore, devo mantenere i dati "ish "mentre attraverso l'albero di sintassi. Ad esempio, durante la compilazione di istruzioni di controllo del flusso, devo generare nomi univoci per il salto delle etichette (etichette generate da un contatore che è passato, aggiornato e restituito, e il vecchio valore del contatore non deve mai essere usato di nuovo) . Un altro esempio è quando trovo letterali di stringhe in linea nell'albero della sintassi, devono essere convertiti permanentemente in variabili heap (in Whitespace, le stringhe sono meglio memorizzate nell'heap). Attualmente sto avvolgendo l'intero modulo di generazione del codice nella monade dello stato per gestirlo.

Mi è stato detto che scrivere un compilatore è un problema che ben si adatta al paradigma funzionale, ma trovo che lo sto progettando più o meno allo stesso modo in cui lo progetterei in C (puoi davvero scrivere C in qualsiasi lingua - anche Haskell con monadi statali).

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

È stato utile?

Soluzione

Direi che lo stato in generale non è un odore di codice, purché sia ??mantenuto piccolo e ben controllato.

Ciò significa che usare monadi come State, ST o su misura, o semplicemente avere una struttura di dati contenente dati di stato che passi in alcuni punti, non è una cosa negativa. (In realtà, le monadi sono solo un aiuto nel fare esattamente questo!) Tuttavia, avere uno stato che va dappertutto (sì, questo significa che tu, monade IO!) È un cattivo odore.

Un esempio abbastanza chiaro di ciò è stato quando il mio team stava lavorando alla nostra partecipazione al ICFP Programming Contest 2009 (il il codice è disponibile su git: //git.cynic.net/haskell/icfp-contest-2009). Abbiamo finito con diverse parti modulari diverse per questo:

  • VM: la macchina virtuale che ha eseguito il programma di simulazione
  • Controller: diversi set di routine che leggono l'output del simulatore e generano nuovi input di controllo
  • Soluzione: generazione del file di soluzione basato sull'output dei controller
  • Visualizzatori: diversi insiemi di routine che leggono sia le porte di input che di output e hanno generato una sorta di visualizzazione o registro di ciò che stava accadendo mentre la simulazione procedeva

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

Il punto chiave qui era che gli interni di ogni stato particolare erano limitati ai loro moduli particolari, e ogni modulo non sapeva nulla nemmeno dell'esistenza di stato per altri moduli. Qualsiasi particolare set di codice e dati stateful era generalmente lungo solo poche decine di righe, con una manciata di elementi di dati nello stato.

Tutto questo è stato 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 chiamavano le cose giuste nell'ordine corretto mentre scorrevano attraverso la simulazione e passavano una quantità molto limitata di informazioni esterne per ciascun modulo (insieme allo stato precedente del modulo, ovviamente).

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

Una risposta dice: " Non usare monadi. " Dal mio punto di vista, questo è esattamente all'indietro. Le monadi sono una struttura di controllo che, tra le altre cose, può aiutarti a ridurre al minimo la quantità di codice che tocca lo stato. Se si considerano i parser monadici come esempio, lo stato dell'analisi (ovvero, il testo in fase di analisi, la distanza in cui ci si è arrivati, eventuali avvisi che si sono accumulati, ecc.) Deve passare attraverso ogni combinatore utilizzato nel parser . Eppure ci saranno solo pochi combinatori che manipolano direttamente lo stato; qualsiasi altra cosa utilizza una di queste poche funzioni. Ciò ti consente di vedere chiaramente e in un unico punto tutta una piccola quantità di codice che può cambiare lo stato e più facilmente la ragione su come può essere modificato, facilitando ancora una volta la gestione.

Altri suggerimenti

Ho scritto più compilatori in Haskell e una monade di stato è una soluzione ragionevole a molti problemi del compilatore. Ma vuoi mantenerlo astratto --- non rendere ovvio che stai usando una monade.

Ecco un esempio del compilatore Haskell di Glasgow (che ho scritto non ; lavoro solo su alcuni bordi), dove costruiamo grafici del flusso di controllo. Ecco i modi di base per creare 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, mantenere una fornitura di etichette uniche è noioso nella migliore delle ipotesi, quindi forniamo anche queste funzioni:

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

L'intera cosa Graph è di tipo astratto, e il traduttore costruisce semplicemente i grafici in modo puramente funzionale, senza essere consapevole del fatto che sta succedendo qualcosa di monadico. Quindi, quando il grafico viene finalmente costruito, al fine di trasformarlo in un tipo di dati algebrico da cui possiamo generare il codice, gli forniamo una fornitura di etichette uniche, eseguiamo la monade dello stato ed estraiamo la struttura dei dati.

La monade dello stato è nascosta sotto; sebbene non sia esposto al client, la definizione di Graph è qualcosa del genere:

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 di stato nascosta dietro uno strato di astrazione, non è affatto maleodorante!

Hai esaminato Grammatiche degli attributi (AG)? (Ulteriori informazioni su wikipedia e un articolo in Monad Reader)?

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

Gli attributi sintetizzati sono cose che generi (o sintetizzi) dal tuo albero di sintassi, questo potrebbe essere il codice generato, o tutti i commenti, o qualsiasi altra cosa ti interessi.

Gli attributi ereditati vengono inseriti nell'albero della sintassi, potrebbe essere l'ambiente o un elenco di etichette da utilizzare durante la generazione del codice.

All'università di Utrecht utilizziamo il Attribute Grammar System ( UUAGC ) per scrivere compilatori. Questo è un pre-processore che genera codice haskell (file .hs ) dai file .ag forniti.


Anche se, se stai ancora imparando Haskell, forse non è questo il momento di iniziare a imparare ancora un altro livello di astrazione.

In tal caso, potresti scrivere manualmente il tipo di codice che gli attributi grammaticali generano per te, ad 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

È possibile che tu voglia un agente applicativo invece di a monade:

http://www.haskell.org/haskellwiki/Applicative_functor

Penso che il documento originale lo spieghi meglio del wiki, tuttavia:

http://www.soi.city.ac. uk / ~ ross / documenti / Applicative.html

Non penso che usare State Monad sia un odore di codice quando usato per modellare lo stato.

Se è necessario passare attraverso lo stato delle proprie funzioni, puoi farlo esplicitamente, prendendo lo stato come argomento e restituendolo in ciascuna funzione. La Monade dello Stato offre una buona astrazione: passa lo stato per te e fornisce molte utili funzioni per combinare funzioni che richiedono stato. In questo caso, usare la Monade dello Stato (o le Applicative) non è un odore di codice.

Tuttavia, se usi la Monade di Stato per emulare uno stile imperativo di programmazione mentre una soluzione funzionale sarebbe sufficiente, stai solo complicando le cose.

In generale dovresti cercare di evitare lo stato ove possibile, ma non è sempre pratico. Applicativo rende il codice efficace più bello e più funzionale, in particolare il codice di attraversamento dell'albero può trarre vantaggio da questo stile. Per il problema della generazione dei nomi è ora disponibile un pacchetto piuttosto carino: valore-fornitura .

Beh, non usare le monadi. Il potere della programmazione funzionale è la purezza delle funzioni e il loro riutilizzo. C'è questo documento che un mio professore ha scritto una volta ed è uno dei ragazzi che hanno contribuito a costruire Haskell.

Il documento si chiama " Perché è importante la programmazione funzionale " ;, ti suggerisco di leggerlo. È una buona lettura.

stiamo attenti alla terminologia qui. Lo stato non è di per sé cattivo; i linguaggi funzionali hanno stato. Che cos'è un "odore di codice"? è quando ti ritrovi a voler assegnare i valori delle variabili e modificarli.

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

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top