Design su larga scala in Haskell? [chiuso]


565

Qual è un buon modo per progettare / strutturare grandi programmi funzionali, specialmente in Haskell?

Ho seguito diversi tutorial (Scrivi a te stesso uno Schema come il mio preferito, con Real World Haskell al secondo posto) - ma la maggior parte dei programmi sono relativamente piccoli e monouso. Inoltre, non considero alcuni di loro particolarmente eleganti (ad esempio, le vaste tabelle di ricerca in WYAS).

Ora voglio scrivere programmi più grandi, con più parti mobili: acquisire dati da una varietà di fonti diverse, pulirli, elaborarli in vari modi, visualizzarli nelle interfacce utente, persistere, comunicare su reti, ecc. Come si potrebbe quale struttura migliore tale codice sia leggibile, gestibile e adattabile alle mutevoli esigenze?

C'è una grande letteratura che affronta queste domande per grandi programmi imperativi orientati agli oggetti. Idee come MVC, modelli di progettazione, ecc. Sono prescrizioni decenti per la realizzazione di obiettivi generali come la separazione delle preoccupazioni e la riusabilità in uno stile OO. Inoltre, i linguaggi imperativi più recenti si prestano a uno stile di refactoring "design as you grow" al quale, secondo la mia opinione da principiante, Haskell sembra meno adatto.

Esiste una letteratura equivalente per Haskell? In che modo lo zoo di strutture di controllo esotiche è disponibile nella programmazione funzionale (monadi, frecce, applicativo, ecc.) Meglio impiegato a questo scopo? Quali migliori pratiche potresti consigliare?

Grazie!

EDIT (questo è un seguito alla risposta di Don Stewart):

@dons menzionato: "Le monadi catturano i progetti architettonici chiave in tipi".

Immagino che la mia domanda sia: come pensare ai progetti architettonici chiave in un linguaggio puramente funzionale?

Considera l'esempio di diversi flussi di dati e diverse fasi di elaborazione. Sono in grado di scrivere parser modulari per i flussi di dati su un insieme di strutture di dati e posso implementare ogni fase di elaborazione come una funzione pura. Le fasi di elaborazione richieste per un dato dipenderanno dal suo valore e da quello di altri. Alcuni passaggi dovrebbero essere seguiti da effetti collaterali come aggiornamenti della GUI o query del database.

Qual è il modo "giusto" per legare i dati e le fasi di analisi in un modo carino? Si potrebbe scrivere una grande funzione che fa la cosa giusta per i vari tipi di dati. Oppure si potrebbe usare una monade per tenere traccia di ciò che è stato elaborato finora e fare in modo che ogni fase di elaborazione ottenga ciò di cui ha bisogno dopo lo stato della monade. Oppure si potrebbe scrivere programmi in gran parte separati e inviare messaggi in giro (questa opzione non mi piace molto).

Le diapositive che ha collegato hanno un proiettile Things we Need: "Idiomi per mappare il design su tipi / funzioni / classi / monadi". Quali sono i modi di dire? :)


9
Penso che l'idea centrale quando si scrivono programmi di grandi dimensioni in un linguaggio funzionale siano i moduli piccoli, specializzati e senza stato che comunicano attraverso il passaggio di messaggi . Ovviamente devi fingere un po 'perché un vero programma ha bisogno di stato. Penso che sia qui che F # brilla su Haskell.
ChaosPandion,

18
@Chaos ma solo Haskell impone l'apolidia per impostazione predefinita. Non hai scelta e devi lavorare sodo per introdurre lo stato (per spezzare la composizionalità) in Haskell :-)
Don Stewart,

7
@ChaosPandion: in teoria non sono in disaccordo. Certamente, in un linguaggio imperativo (o in un linguaggio funzionale progettato attorno al passaggio dei messaggi), potrebbe benissimo essere quello che farei. Ma Haskell ha altri modi per gestire lo stato, e forse mi permettono di mantenere maggiori benefici "puri".
Dan,

1
Ho scritto un po 'su questo in "Linee guida di progettazione" in questo documento: community.haskell.org/~ndm/downloads/…
Neil Mitchell,

5
@JonHarrop non dimentichiamo che mentre MLOC è una buona metrica quando si confrontano progetti in lingue simili, non ha molto senso il confronto tra lingue, specialmente con lingue come Haskell, dove il riutilizzo del codice e la modularità sono molto più facili e sicuri rispetto ad alcune lingue là fuori.
Tair,

Risposte:


519

Ne parlo un po 'in Engineering Large Projects in Haskell e nella progettazione e realizzazione di XMonad. L'ingegneria in generale riguarda la gestione della complessità. I principali meccanismi di strutturazione del codice in Haskell per la gestione della complessità sono:

Il sistema di tipi

  • Utilizzare il sistema di tipi per imporre astrazioni, semplificando le interazioni.
  • Applicare invarianti chiave tramite tipi
    • (ad es. che determinati valori non possono sfuggire a un certo ambito)
    • Quel codice certo non fa IO, non tocca il disco
  • Applica sicurezza: verificate le eccezioni (forse / entrambi), evita di mescolare concetti (Word, Int, Address)
  • Buone strutture di dati (come le cerniere lampo) possono rendere inutili alcune classi di test, in quanto escludono staticamente errori fuori limite.

Il profiler

  • Fornire prove oggettive dell'heap e dei profili temporali del programma.
  • La profilazione dell'heap, in particolare, è il modo migliore per garantire l'uso inutile della memoria.

Purezza

  • Ridurre drasticamente la complessità rimuovendo lo stato. Il codice puramente funzionale si ridimensiona, perché è composizionale. Tutto ciò di cui hai bisogno è il tipo per determinare come usare un po 'di codice - non si romperà misteriosamente quando cambi qualche altra parte del programma.
  • Usa un sacco di programmazione in stile "modello / vista / controller": analizza i dati esterni il più presto possibile in strutture di dati puramente funzionali, opera su tali strutture, quindi una volta fatto tutto il lavoro, esegui il rendering / svuotamento / serializzazione. Mantiene pura la maggior parte del codice

analisi

  • QuickCheck + Copertura del codice Haskell, per assicurarti di testare le cose che non puoi verificare con i tipi.
  • GHC + RTS è ottimo per vedere se stai trascorrendo troppo tempo a fare GC.
  • QuickCheck può anche aiutarti a identificare API pulite e ortogonali per i tuoi moduli. Se le proprietà del tuo codice sono difficili da dichiarare, probabilmente sono troppo complesse. Continua a eseguire il refactoring fino a quando non hai un set pulito di proprietà in grado di testare il tuo codice, che si componga bene. Quindi anche il codice è probabilmente ben progettato.

Monadi per la strutturazione

  • Le monadi catturano i progetti architettonici chiave in tipi (questo codice accede all'hardware, questo codice è una sessione per utente singolo, ecc.)
  • Ad esempio la monade X in xmonad, cattura esattamente il design per quale stato è visibile a quali componenti del sistema.

Digitare classi e tipi esistenziali

  • Utilizzare le classi di tipi per fornire l'astrazione: nascondere le implementazioni dietro le interfacce polimorfiche.

Concorrenza e parallelismo

  • Entra parnel tuo programma per battere la concorrenza con un parallelismo facile e compostabile.

Refactor

  • Puoi fare il refactoring in Haskell molto . I tipi assicurano che le modifiche su larga scala siano sicure, se si utilizzano saggiamente i tipi. Questo aiuterà il tuo ridimensionamento della base di codice. Assicurati che i tuoi refactoring causeranno errori di tipo fino al completamento.

Usa l'FFI con saggezza

  • FFI semplifica la riproduzione con codice esterno, ma tale codice esterno può essere pericoloso.
  • Prestare molta attenzione alle ipotesi sulla forma dei dati restituiti.

Meta programmazione

  • Un po 'di Template Haskell o generici possono rimuovere il boilerplate.

Imballaggio e distribuzione

  • Usa la cabala. Non creare il tuo sistema di compilazione. (EDIT: In realtà probabilmente vorrai usare Stack ora per iniziare.).
  • Usa Haddock per buoni documenti API
  • Strumenti come graphmod possono mostrare le strutture del tuo modulo.
  • Affidati alle versioni della piattaforma Haskell di librerie e strumenti, se possibile. È una base stabile. (EDIT: Ancora una volta, in questi giorni probabilmente vorrai usare Stack per ottenere una base stabile e funzionante.)

Avvertenze

  • Utilizzare -Wallper mantenere il codice pulito dagli odori. Potresti anche guardare Agda, Isabelle o Catch per maggiori garanzie. Per un controllo simile a una lanugine, consulta l' hlint , che suggerirà miglioramenti.

Con tutti questi strumenti puoi gestire la complessità, rimuovendo quante più interazioni possibili tra i componenti. Idealmente, hai una base molto ampia di codice puro, che è davvero facile da mantenere, poiché è compositiva. Questo non è sempre possibile, ma vale la pena puntare.

In generale: decomporre le unità logiche del sistema nei componenti referenzialmente più piccoli possibili, quindi implementarli in moduli. Gli ambienti globali o locali per insiemi di componenti (o componenti interni) potrebbero essere mappati su monadi. Utilizzare tipi di dati algebrici per descrivere le strutture di dati di base. Condividi ampiamente queste definizioni.


8
Grazie Don, la tua risposta è eccellente - queste sono tutte linee guida preziose e le farò regolarmente riferimento. Immagino che la mia domanda si verifichi un passo prima che uno abbia bisogno di tutto questo, comunque. Quello che mi piacerebbe davvero sapere sono gli "Idiomi per mappare il design su tipi / funzioni / classi / monadi" ... Potrei provare a inventare il mio, ma speravo che ci potesse essere un insieme di migliori pratiche distillate da qualche parte - o, in caso contrario, raccomandazioni per la lettura di un codice ben strutturato di un sistema di grandi dimensioni (al contrario, diciamo, di una libreria focalizzata). Ho modificato il mio post per porre questa stessa domanda in modo più diretto.
Dan,

6
Ho aggiunto del testo sulla decomposizione del design ai moduli. Il tuo obiettivo è identificare le funzioni logicamente correlate in moduli che hanno interfacce referenzialmente trasparenti con altre parti del sistema e utilizzare tipi di dati puramente funzionali il più presto possibile, per quanto possibile, per modellare il mondo esterno in modo sicuro. Il documento di progettazione di xmonad tratta molto di questo: xmonad.wordpress.com/2009/09/09/…
Don Stewart,

3
Ho provato a scaricare le diapositive dai progetti di Engineering Large Projects in Haskell , ma il collegamento sembrava interrotto. Eccone uno funzionante: galois.com/~dons/talks/dons-londonhug-decade.pdf
mik01aj

3
Sono riuscito a trovare questo nuovo link per il download: pau-za.cz/data/2/sprava.pdf
Riccardo T.

3
@Heather Anche se il link per il download nella pagina che ho menzionato nel commento prima non funziona, sembra che le diapositive possano ancora essere visualizzate su scribd: scribd.com/doc/19503176/The-Design-and-Implementation-of -xmonad
Riccardo T.

118

Don ti ha dato la maggior parte dei dettagli di cui sopra, ma qui sono i miei due centesimi dal fare programmi statali davvero nitidi come i demoni di sistema in Haskell.

  1. Alla fine, vivi in ​​una pila di trasformatori di monade. In fondo c'è IO. Inoltre, ogni modulo principale (in senso astratto, non nel senso di un modulo in un file) mappa il suo stato necessario in uno strato in quello stack. Quindi se hai il tuo codice di connessione al database nascosto in un modulo, scrivi tutto per essere su un tipo Connessione MonadReader m => ... -> m ... e quindi le tue funzioni di database possono sempre ottenere la loro connessione senza funzioni di altri i moduli devono essere consapevoli della sua esistenza. Potresti finire con un livello che trasporta la tua connessione al database, un altro la tua configurazione, un terzo i tuoi vari semafori e mvar per la risoluzione del parallelismo e della sincronizzazione, un altro gestisce i tuoi file di registro, ecc.

  2. Capire gestire il vostro errore di prima . La più grande debolezza al momento per Haskell nei sistemi più grandi è la pletora di metodi di gestione degli errori, compresi quelli schifosi come Forse (il che è sbagliato perché non puoi restituire alcuna informazione su cosa è andato storto; usa sempre O invece di Forse a meno che tu non sia davvero significa solo valori mancanti). Scopri come lo farai prima e imposta gli adattatori dai vari meccanismi di gestione degli errori che le tue librerie e altri codici usano nel tuo ultimo. Questo ti salverà un mondo di dolore in seguito.

Addendum (estratto dai commenti; grazie a Lii e liminalisht ) -
ulteriori discussioni su diversi modi per dividere un grande programma in monadi in una pila:

Ben Kolera offre una grande introduzione pratica a questo argomento e Brian Hurt discute le soluzioni al problema di liftinserire azioni monadiche nella tua monade personalizzata. George Wilson mostra come usare mtlper scrivere il codice che funziona con qualsiasi monade che implementa i caratteri tipografici richiesti, piuttosto che il tuo tipo di monade personalizzato. Carlo Hamalainen ha scritto alcune brevi e utili note che riassumono il discorso di George.


5
Due buoni punti! Questa risposta ha il merito di essere ragionevolmente concreta, qualcosa che gli altri non lo sono. Sarebbe interessante leggere ulteriori discussioni su diversi modi per dividere un grande programma in monadi in una pila. Si prega di pubblicare collegamenti a tali articoli se ne avete!
Lii,

6
@Lii Ben Kolera offre una grande introduzione pratica a questo argomento, e Brian Hurt discute le soluzioni al problema di liftinserire azioni monadiche nella tua monade personalizzata. George Wilson mostra come usare mtlper scrivere il codice che funziona con qualsiasi monade che implementa i caratteri tipografici richiesti, piuttosto che il tuo tipo di monade personalizzato. Carlo Hamalainen ha scritto alcune brevi e utili note che riassumono il discorso di George.
liminalisht,

Concordo sul fatto che le pile di trasformatori di monade tendono ad essere le basi architettoniche chiave, ma mi sforzo molto di tenere le I / O fuori da esse. Non è sempre possibile, ma se pensi a cosa significhi "e poi" nella tua monade potresti scoprire che hai davvero una continuazione o un automa da qualche parte nella parte inferiore che può quindi essere interpretato in IO da una funzione "run".
Paul Johnson,

Come ha già sottolineato @PaulJohnson, questo approccio Monad Transformer Stack sembra in conflitto con il ReaderT Design Pattern di
McBear Holden,

43

Progettare programmi di grandi dimensioni in Haskell non è così diverso dal farlo in altre lingue. La programmazione in generale riguarda la suddivisione del problema in pezzi gestibili e il modo di adattarli insieme; il linguaggio di implementazione è meno importante.

Detto questo, in un design di grandi dimensioni è bello provare a sfruttare il sistema di tipi per assicurarsi di poter adattare i pezzi solo in modo corretto. Ciò potrebbe comportare tipi di tipo nuovo o fantasma per rendere le cose che sembrano avere lo stesso tipo essere diverse.

Quando si tratta di refactoring del codice mentre si procede, la purezza è un grande vantaggio, quindi cerca di mantenere puro quanto più codice possibile. Il codice puro è facile da refactoring, perché non ha interazioni nascoste con altre parti del programma.


14
In realtà ho scoperto che il refactoring è piuttosto frustrante, se i tipi di dati devono cambiare. Richiede noiosamente la modifica dell'arità di molti costruttori e corrispondenze di schemi. (Concordo sul fatto che il refactoring di funzioni pure in altre funzioni pure dello stesso tipo è facile - purché non si tocchino i tipi di dati)
Dan

2
@Dan Puoi liberarti completamente gratuitamente con piccole modifiche (come aggiungere semplicemente un campo) quando usi i record. Alcuni potrebbero voler prendere l'abitudine dei dischi (io sono uno di loro ^^ ").
MasterMastic

5
@Dan intendo che se cambi il tipo di dati di una funzione in qualsiasi lingua non devi fare lo stesso? Non vedo come un linguaggio come Java o C ++ possa aiutarti in questo senso. Se dici che puoi usare una sorta di interfaccia comune a cui entrambi i tipi obbediscono, allora avresti dovuto farlo con Typeclasses in Haskell.
punto

4
@semicon la differenza per linguaggi come Java è l'esistenza di strumenti maturi, ben collaudati e completamente automatizzati per il refactoring. Generalmente questi strumenti hanno una fantastica integrazione con gli editor e portano via un'enorme quantità del noioso lavoro associato al refactoring. Haskell ci offre un sistema di tipo brillante con cui rilevare cose che devono essere cambiate in un refactoring, ma gli strumenti per realizzare effettivamente quel refactoring sono (al momento) molto limitati, specialmente rispetto a ciò che è già stato disponibile in Java ecosistema per più di 10 anni.
jsk,

16

Ho imparato la programmazione funzionale strutturata la prima volta con questo libro . Potrebbe non essere esattamente quello che stai cercando, ma per i principianti nella programmazione funzionale, questo potrebbe essere uno dei migliori primi passi per imparare a strutturare i programmi funzionali - indipendentemente dalla scala. A tutti i livelli di astrazione, il progetto dovrebbe sempre avere strutture chiaramente organizzate.

Il mestiere della programmazione funzionale

Il mestiere della programmazione funzionale

http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/


11
Per quanto il Craft of FP sia - ho imparato Haskell da esso - è un testo introduttivo per programmatori principianti , non per la progettazione di grandi sistemi in Haskell.
Don Stewart,

3
Bene, è il miglior libro che conosco sulla progettazione di API e come nascondere i dettagli dell'implementazione. Con questo libro sono diventato un programmatore migliore in C ++ - solo perché ho imparato modi migliori per organizzare il mio codice. Bene, la tua esperienza (e risposta) è sicuramente migliore di questo libro, ma Dan potrebbe probabilmente essere ancora un principiante in Haskell. ( where beginner=do write $ tutorials `about` Monads)
comonad,

11

Attualmente sto scrivendo un libro dal titolo "Design funzionale e architettura". Ti fornisce una serie completa di tecniche su come costruire una grande applicazione usando un approccio puramente funzionale. Descrive molti modelli e idee funzionali durante la costruzione di un'applicazione simile a SCADA "Andromeda" per controllare da zero le astronavi. La mia lingua principale è l'Haskell. Il libro copre:

  • Approcci alla modellazione dell'architettura mediante diagrammi;
  • Analisi dei requisiti;
  • Modellazione di domini DSL integrata;
  • Progettazione e realizzazione DSL esterne;
  • Monadi come sottosistemi con effetti;
  • Monadi libere come interfacce funzionali;
  • EDSL con frecce;
  • Inversione del controllo usando eDSL monadici liberi;
  • Memoria transazionale software;
  • Lenti a contatto;
  • Monadi di stato, lettore, scrittore, RWS, ST;
  • Stato impuro: IORef, MVar, STM;
  • Multithreading e modellazione di domini simultanei;
  • GUI;
  • Applicabilità di tecniche e approcci tradizionali come UML, SOLID, GRASP;
  • Interazione con sottosistemi impuri.

È possibile acquisire familiarità con il codice del libro qui e il codice del progetto "Andromeda" .

Mi aspetto di finire questo libro alla fine del 2017. Fino a quando ciò non accadrà, puoi leggere il mio articolo "Design e architettura nella programmazione funzionale" (Rus) qui .

AGGIORNARE

Ho condiviso il mio libro online (primi 5 capitoli). Vedi post su Reddit


Alexander, potresti gentilmente aggiornare questa nota quando il tuo libro sarà completo, così potremmo seguirlo. Saluti.
Max

4
Sicuro! Per ora ho finito la metà del testo, ma è un 1/3 del lavoro complessivo. Quindi, mantieni il tuo interesse, questo mi ispira molto!
graninas,

2
Ciao! Ho condiviso il mio libro online (solo i primi 5 capitoli). Vedi post su Reddit: reddit.com/r/haskell/comments/6ck72h/…
graninas

grazie per la condivisione e il lavoro!
Max

Non vedo l'ora!
patriques,

7

Il post sul blog di Gabriel Vale la pena menzionare le architetture di programmi scalabili .

I modelli di design Haskell differiscono dai modelli di design tradizionali in un modo importante:

  • Architettura convenzionale : combinare più componenti insieme di tipo A per generare una "rete" o "topologia" di tipo B

  • Architettura di Haskell : combina diversi componenti insieme di tipo A per generare un nuovo componente dello stesso tipo A, di carattere indistinguibile dalle sue parti sostituenti

Mi sembra spesso che un'architettura apparentemente elegante tende spesso a sfuggire alle biblioteche che mostrano questo bel senso di omogeneità, in un modo dal basso verso l'alto. In Haskell questo è particolarmente evidente: i modelli che sarebbero tradizionalmente considerati "architettura top-down" tendono ad essere catturati in librerie come mvc , Netwire e Cloud Haskell . Vale a dire, spero che questa risposta non venga interpretata come un tentativo di sostituire nessuno degli altri in questo thread, solo che le scelte strutturali possono e dovrebbero idealmente essere sottratte nelle biblioteche dagli esperti del dominio. La vera difficoltà nella costruzione di grandi sistemi, secondo me, sta valutando queste biblioteche sulla loro "bontà" architettonica rispetto a tutte le vostre preoccupazioni pragmatiche.

Come menziona liminalisht nei commenti, Il modello di design della categoria è un altro post di Gabriel sull'argomento, in una vena simile.


3
Vorrei menzionare un altro post di Gabriel Gonzalez sul modello di design della categoria . Il suo argomento di base è che ciò che noi programmatori funzionali consideriamo "buona architettura" è in realtà "architettura compositiva" - sta progettando programmi usando elementi che sono garantiti per comporre. Poiché le leggi di categoria garantiscono che l'identità e l'associatività siano preservate nella composizione, un'architettura compositiva si ottiene utilizzando astrazioni per le quali abbiamo una categoria - ad es. Funzioni pure, azioni monadiche, pipe, ecc.
liminalisht


3

Forse devi fare un passo indietro e pensare a come tradurre la descrizione del problema in un progetto in primo luogo. Poiché Haskell è di così alto livello, può catturare la descrizione del problema sotto forma di strutture di dati, le azioni come procedure e la pura trasformazione come funzioni. Quindi hai un design. Lo sviluppo inizia quando compili questo codice e trovi errori concreti su campi mancanti, istanze mancanti e trasformatori monadici mancanti nel tuo codice, perché ad esempio esegui un accesso al database da una libreria che necessita di una certa monade di stato all'interno di una procedura IO. E voilà, c'è il programma. Il compilatore alimenta i tuoi schizzi mentali e dà coerenza al design e allo sviluppo.

In questo modo beneficiate dell'aiuto di Haskell sin dall'inizio e la codifica è naturale. Non mi interessa fare qualcosa di "funzionale" o "puro" o abbastanza generale se ciò che hai in mente è un problema ordinario concreto. Penso che l'eccessiva ingegneria sia la cosa più pericolosa nell'IT. Le cose sono diverse quando il problema è creare una libreria che astragga una serie di problemi correlati.

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.