Perché i programmi funzionali hanno una correlazione tra successo della compilazione e correttezza?


12

Sono 4 anni che vado alla programmazione funzionale, da quando ho iniziato a lavorare con LINQ. Di recente, ho scritto del puro codice C # funzionale e ho notato, in prima persona, ciò che ho letto sui programmi funzionali - che una volta compilati tendono ad essere corretti.

Ho provato a mettere un dito sul perché questo è il caso, ma non ci sono riuscito.

Un'ipotesi è che nell'applicare le entità OO, si ha uno "strato di astrazione" che non è presente nei programmi funzionali e questo strato di astrazione rende possibile che i contratti tra gli oggetti siano corretti mentre l'implementazione è sbagliata.

Qualcuno ha pensato a questo e ha trovato la ragione astratta alla base della correlazione tra successo della compilazione e correttezza del programma nella programmazione funzionale?


9
Lisp è un linguaggio funzionale, ma non ha controlli del tempo di compilazione di cui parlare. Lo stesso per alcune altre lingue funzionali. Una caratterizzazione più accurata delle lingue di cui parli sarebbe: Lingue con potenti sistemi di tipo formale (almeno Hindley-Milner).

1
@delnan Non direi che Lisp sia un linguaggio di programmazione funzionale, sebbene possa essere usato per scrivere codice di programmazione funzionale. Clojure che è un dialetto Lisp è un linguaggio di programmazione funzionale
sakisk,

2
Sono d'accordo con @delnan. Questa affermazione è più correlata ai linguaggi di programmazione funzionale tipicamente statici, in particolare Haskell che utilizza il sistema Hindley-Milner. Penso che l'idea principale sia che se ottieni i tipi giusti, aumenta la sicurezza che il tuo programma sia corretto.
sakisk,

1
Il codice funzionale può avere altrettante astrazioni e riferimenti indiretti come il tipico codice OOP tradizionale (se non di più). Il diavolo è nei dettagli: meno effetti collaterali e nessun null significa meno stato invisibile da tracciare e meno possibilità di rovinare. Nota che puoi applicare quegli stessi principi nei linguaggi imperativi tradizionali, è solo più lavoro e spesso più prolisso (ad es. Dover schiaffeggiare finaltutto).
Doval,

1
Non una risposta completa, ma la digitazione del programma è una forma grezza di verifica formale del programma. In generale, i programmi orientati agli oggetti hanno sistemi di tipo complicati o molto semplici in quanto la sostituzione deve essere presa in considerazione - nella maggior parte dei casi sono resi non sicuri per comodità. OTOH i sistemi simil-ML possono usare CH in modo completo in modo da poter codificare una prova solo nei tipi e usare il compilatore come correttore di prove.
Maciej Piechotka,

Risposte:


12

Posso scrivere questa risposta come qualcuno che dimostra molto le cose, quindi per me la correttezza non è solo ciò che funziona, ma ciò che funziona ed è facile da dimostrare.

In molti sensi, la programmazione funzionale è più restrittiva della programmazione imperativa. Dopotutto, nulla ti impedisce di non mutare mai una variabile in C! In effetti, la maggior parte delle funzionalità dei linguaggi FP sono di cui si può parlare in termini di poche funzionalità di base. Tutto si riduce praticamente a lambda, applicazione di funzioni e abbinamento di modelli!

Tuttavia, poiché abbiamo pagato anticipatamente il pifferaio, abbiamo molto meno da affrontare e abbiamo molte meno opzioni su come le cose possono andare storte. Se sei un fan del 1984, la libertà è davvero una schiavitù! Usando 101 trucchi diversi per un programma, dobbiamo ragionare sulle cose come se una di queste 101 cose potesse accadere! È davvero difficile da fare a quanto pare :)

Se inizi con le forbici di sicurezza anziché con la spada, correre è moderatamente meno pericoloso.

Ora esaminiamo la tua domanda: come si inserisce tutto ciò nel "si compila e funziona!" fenomeni. Penso che gran parte di ciò sia lo stesso motivo per cui è facile provare il codice! Dopotutto, quando scrivi software stai costruendo alcune prove informali che sono corrette. Per questo motivo, ciò che è coperto dalle tue prove manuali fatte a mano e dalla nozione di correttezza (controllo dei caratteri) dei compilatori è parecchio.

Man mano che aggiungi funzionalità e interazioni complicate tra loro, aumenta ciò che non viene controllato dal sistema di tipi. Tuttavia, la tua capacità di costruire prove informali non sembra migliorare! Ciò significa che c'è molto di più che può passare attraverso l'ispezione iniziale e deve essere catturato in seguito.


1
Mi piace la tua risposta, ma non vedo come risponda alla domanda dell'OP
sakisk,

1
@faif Ampliata la mia risposta. TLDR: tutti sono matematici.
Daniel Gratzer,

"Usando 101 trucchi diversi per un programma, dobbiamo ragionare sulle cose come se potesse accadere una di queste 101 cose!": Ho letto da qualche parte che devi essere un genio per programmare con la mutazione, perché devi tenerlo molte informazioni nella tua testa.
Giorgio,

12

la ragione astratta alla base della correlazione tra successo della compilazione e correttezza del programma nella programmazione funzionale?

Stato mutevole.

I compilatori controllano le cose staticamente. Assicurano che il tuo programma sia ben formato e il sistema dei tipi fornisce un meccanismo per cercare di garantire che il giusto tipo di valori sia consentito nel giusto tipo di luoghi. Il sistema dei tipi cerca anche di garantire che il giusto tipo di semantica sia consentito nel giusto tipo di luoghi.

Non appena il programma introduce lo stato, quest'ultimo vincolo diventa meno utile. Non solo devi preoccuparti dei valori giusti nei punti giusti, ma devi anche tener conto della modifica di quel valore in punti arbitrari del tuo programma. Devi tenere conto della semantica del tuo codice che cambia insieme a quello stato.

Se stai facendo bene la programmazione funzionale, non c'è stato (o molto poco) stato mutabile.

C'è qualche dibattito sulla causa qui - se i programmi senza stato funzionano dopo la compilazione più frequentemente perché il compilatore può catturare più bug o se i programmi senza stato funzionano più frequentemente dopo la compilazione perché quello stile di programmazione produce meno bug.

È probabilmente un mix di entrambi nella mia esperienza.


2
"È probabilmente un mix di entrambi nella mia esperienza.": Ho la stessa esperienza. La digitazione statica rileva errori in fase di compilazione anche quando si utilizza un linguaggio imperativo (ad esempio Pascal). In FP, l'evitamento della mutabilità e, aggiungerei, l'uso di uno stile di programmazione più dichiarativo rende più facile ragionare sul codice. Se una lingua offre entrambi, otterrai entrambi i vantaggi.
Giorgio,

7

Per dirla semplicemente, le restrizioni significano che ci sono meno modi corretti per mettere insieme le cose, e le funzioni di prima classe rendono più facile fattorizzare cose come le strutture ad anello. Prendi il ciclo da questa risposta , ad esempio:

for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        iterator.remove();
    }
}

Questo sembra essere l'unico modo imperativo sicuro in Java per rimuovere un elemento da una raccolta mentre lo stai ripetendo. Ci sono molti modi che sembrano molto vicini, ma sono sbagliati. Le persone ignare di questo metodo a volte passano attraverso modi contorti per evitare il problema, come invece scorrere una copia.

Non è terribilmente difficile rendere questo generico, quindi funzionerà su più di semplici raccolte di Strings, ma senza funzioni di prima classe, non è possibile sostituire il predicato (la condizione all'interno di if), quindi questo codice tende a essere copiato e incollato e leggermente modificato.

Combina funzioni di prima classe che ti danno la possibilità di passare il predicato come parametro, con la restrizione dell'immutabilità che lo rende molto fastidioso se non lo fai, e ti vengono in mente semplici blocchi come filter, come in questo codice Scala quello fa la stessa cosa:

list filter (!_.isEmpty)

Ora pensa a ciò che il sistema di tipi controlla per te, al momento della compilazione nel caso di Scala, ma questi controlli vengono eseguiti anche dai sistemi di tipi dinamici la prima volta che lo esegui:

  • listdeve essere una sorta di tipo che supporta il filtermetodo, vale a dire un insieme.
  • Gli elementi di listdevono avere un isEmptymetodo che restituisce un valore booleano.
  • L'output sarà una raccolta (potenzialmente) più piccola con lo stesso tipo di elementi.

Una volta verificate queste cose, quali altri modi sono rimasti per il programmatore da rovinare? Ho dimenticato accidentalmente il !, che ha causato un errore del test case estremamente evidente. Questo è praticamente l'unico errore disponibile da fare, e l'ho fatto solo perché stavo traducendo direttamente dal codice testato per la condizione inversa.

Questo modello viene ripetuto più volte. Le funzioni di prima classe ti consentono di trasformare le cose in piccole utility riutilizzabili con una semantica precisa, restrizioni come l'immutabilità ti danno l'impulso di farlo e il controllo dei tipi di tali utility lascia poco spazio per rovinarle.

Naturalmente, tutto dipende dal fatto che il programmatore sappia che la funzione di semplificazione come filtergià esiste e che sia in grado di trovarla o di riconoscere il vantaggio di crearne una da soli. Prova a implementarlo da solo ovunque usando solo la ricorsione della coda e sei di nuovo nella stessa barca di complessità della versione imperativa, solo peggio. Solo perché puoi scriverlo in modo molto semplice, non significa che la versione semplice sia ovvia.


"Una volta verificate queste cose, quali altri modi sono rimasti per il programmatore da rovinare?": Questo in qualche modo conferma la mia esperienza che (1) la digitazione statica + (2) lo stile funzionale lasciano meno modi di rovinare le cose. Di conseguenza tendo a ottenere un programma corretto più velocemente e ho bisogno di scrivere meno unit test quando si utilizza FP.
Giorgio,

2

Non credo che ci sia una correlazione significativa tra la compilazione della programmazione funzionale e la correttezza del runtime. Potrebbe esserci una correlazione tra la compilazione tipizzata staticamente e la correttezza del runtime, poiché almeno potresti avere i tipi giusti, se non stai eseguendo il casting.

L'aspetto del linguaggio di programmazione che può in qualche modo correlare la compilazione riuscita con la correttezza del tipo di runtime, come descrivi, è la tipizzazione statica e anche in questo caso, solo se non stai indebolendo il controllo del tipo con i cast che possono essere affermati solo in fase di runtime (in ambienti con valori o luoghi fortemente tipizzati, ad esempio Java o .Net) o per niente (in ambienti in cui le informazioni sul tipo vengono perse o con digitazione debole, ad esempio C e C ++).

Tuttavia, la programmazione funzionale in sé può aiutare in altri modi, come evitare dati condivisi e stato mutevole.

Entrambi gli aspetti insieme possono avere una correlazione significativa nella correttezza, ma devi essere consapevole del fatto che non avere errori di compilazione e di runtime non dice nulla, a rigor di termini, sulla correttezza in un senso più ampio, come nel programma fa quello che dovrebbe fare e fallisce velocemente per input non valido o errore di runtime incontrollabile. Per questo, sono necessarie regole aziendali, requisiti, casi d'uso, asserzioni, unit test, test di integrazione, ecc. Alla fine, almeno secondo me, forniscono molta più sicurezza rispetto alla programmazione funzionale, alla tipizzazione statica o ad entrambi.


Questo. La correttezza di un programma non può essere giudicata da una compilazione corretta. Se un compilatore fosse in grado di comprendere i requisiti spesso contrastanti e imprecisi di ogni persona che ha contribuito alle specifiche del programma, forse la compilazione corretta potrebbe essere considerata corretta. Ma quel mitico compilatore non avrebbe bisogno di un programmatore! Mentre potrebbe esserci una correlazione complessiva leggermente più elevata tra compilazione e correttezza per programmi funzionali rispetto a programmi imperativi, è una parte così piccola del giudizio di correttezza totale che penso sia sostanzialmente irrilevante
Jordan Rieger,

2

Spiegazione per i dirigenti:

Un programma funzionale è come una grande macchina in cui tutto è collegato, tubi, cavi. [Un'automobile]

Un programma procedurale è come un edificio con stanze contenenti una piccola macchina, che immagazzina i prodotti parziali in bidoni, ottenendo prodotti parziali da altrove. [Un'industria]

Quindi quando la macchina funzionale si adatta già insieme: è destinata a produrre qualcosa. Se viene eseguito un complesso procedurale, potresti avere effetti specifici, introdurre il caos, non garantirne il funzionamento. Anche se hai una lista di controllo di tutto ciò che è correttamente integrato, ci sono così tanti stati, situazioni possibili (prodotti parziali in giro, secchi traboccanti, mancanti), che le garanzie sono difficili da dare.


Ma seriamente, il codice procedurale non specifica la semantica del risultato desiderato tanto quanto il codice funzionale. I programmatori procedurali possono più facilmente cavarsela con codice e dati circostanziali e introdurre diversi modi per fare una cosa (alcuni dei quali imperfetti). In genere vengono creati dati estranei. I programmatori funzionali potrebbero impiegare più tempo quando il problema diventa più complesso?

Un linguaggio funzionale tipizzato in modo forte può ancora fare dati e analisi del flusso migliori. Con un linguaggio procedurale, l'obiettivo di un programma deve spesso essere definito al di fuori del programma, come un'analisi formale di correttezza.


1
Migliore anologia: la programmazione funzionale è come un help desk senza clienti: tutto è meraviglioso (purché non si metta in discussione lo scopo o l'efficienza).
Brendan,

L'auto e la fabbrica di @Brendan non formano una cattiva analogia. Cerca di spiegare perché i programmi (su piccola scala) in un linguaggio funzionale hanno maggiori probabilità di funzionare e sono meno soggetti a errori di una "fabbrica". Ma in soccorso di OOP arriva che una fabbrica può produrre diverse cose ed è più grande. Il tuo confronto è appropriato; quanto spesso si sente FP può parallelizzare e ottimizzare enormemente ma in effetti (nessun gioco di parole) produce risultati lenti. Continuo a detenere FP.
Joop Eggen,

La programmazione funzionale su larga scala funziona abbastanza bene per un en.wikipedia.org/wiki/Spherical_cow Keep it local.
Den

@Den io stesso non temerei problemi di fattibilità lavorando su un progetto FP su larga scala. Anche adorarlo. La generalizzazione ha i suoi limiti. Ma poiché anche quest'ultima affermazione è una generalizzazione ... (grazie per la mucca sferica)
Joop Eggen,
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.