Cosa significano i termini programmazione funzionale, dichiarativa e imperativa?
Cosa significano i termini programmazione funzionale, dichiarativa e imperativa?
Risposte:
Al momento della stesura di questo, le risposte più votate in questa pagina sono imprecise e confuse sulla definizione dichiarativa vs. imperativa, inclusa la risposta che cita Wikipedia. Alcune risposte stanno combinando i termini in diversi modi.
Fai riferimento anche alla mia spiegazione del perché la programmazione del foglio di calcolo è dichiarativa, indipendentemente dal fatto che le formule mutino le celle.
Inoltre, diverse risposte affermano che la programmazione funzionale deve essere un sottoinsieme di dichiarativi. Da quel punto dipende se differenziamo la "funzione" dalla "procedura". Consente di gestire prima l'imperativo e il dichiarativo.
Definizione di espressione dichiarativa
L' unico attributo che può eventualmente differenziare un'espressione dichiarativa da un'espressione imperativa è la trasparenza referenziale (RT) delle sue sottoespressioni. Tutti gli altri attributi sono condivisi tra entrambi i tipi di espressioni o derivati dalla RT.
Un linguaggio dichiarativo al 100% (cioè uno in cui ogni espressione possibile è RT) non (tra gli altri requisiti di RT) non consente la mutazione dei valori memorizzati, ad esempio HTML e la maggior parte di Haskell.
Definizione di espressione RT
RT è spesso definito come "privo di effetti collaterali". Il termine effetti non ha una definizione precisa, quindi alcune persone non concordano sul fatto che "nessun effetto collaterale" è uguale a RT. RT ha una definizione precisa .
Poiché ogni sottoespressione è concettualmente una chiamata di funzione, RT richiede che l'implementazione di una funzione (ovvero le espressioni all'interno della funzione chiamata) non possa accedere allo stato mutabile esterno alla funzione (l'accesso allo stato locale mutabile è permesso). In parole povere, la funzione (implementazione) dovrebbe essere pura .
Definizione di pura funzione
Si dice spesso che una funzione pura non abbia "effetti collaterali". Il termine effetti non ha una definizione precisa, quindi alcune persone non sono d'accordo.
Le funzioni pure hanno i seguenti attributi.
Ricorda che RT si applica alle espressioni (che include le chiamate di funzione) e la purezza si applica alle (implementazioni di) funzioni.
Un esempio oscuro di funzioni impure che creano espressioni RT è la concorrenza, ma ciò è dovuto al fatto che la purezza è interrotta a livello di astrazione di interruzione. Non hai davvero bisogno di saperlo. Per creare espressioni RT, chiamate funzioni pure.
Attributi derivati di RT
Qualsiasi altro attributo citato per la programmazione dichiarativa, ad esempio la citazione del 1999 usata da Wikipedia, deriva da RT o è condiviso con la programmazione imperativa. Dimostrando così che la mia definizione precisa è corretta.
Nota, l'immutabilità di valori esterni è un sottoinsieme dei requisiti per RT.
I linguaggi dichiarativi non hanno strutture di controllo in loop, ad esempio for
e while
, poiché a causa dell'immutabilità , la condizione del loop non cambierebbe mai.
I linguaggi dichiarativi non esprimono un flusso di controllo diverso dall'ordine delle funzioni nidificate (ovvero dipendenze logiche), perché a causa dell'immutabilità , altre scelte dell'ordine di valutazione non cambiano il risultato (vedere sotto).
I linguaggi dichiarativi esprimono "passi" logici (cioè l'ordine di chiamata della funzione RT nidificato), ma se ogni chiamata di funzione è un semantico di livello superiore (ovvero "cosa fare") non è un requisito della programmazione dichiarativa. La distinzione dall'imperativo è che a causa dell'immutabilità (cioè più in generale RT), questi "passi" non possono dipendere dallo stato mutabile, piuttosto solo dall'ordine relazionale della logica espressa (cioè dall'ordine di annidamento delle chiamate di funzione, ovvero sottoespressioni ).
Ad esempio, il paragrafo HTML <p>
non può essere visualizzato fino a quando non sono state valutate le sottoespressioni (ovvero i tag) nel paragrafo. Non esiste uno stato mutabile, ma solo una dipendenza dell'ordine dovuta alla relazione logica della gerarchia di tag (annidamento di sottoespressioni, che sono chiamate di funzione annidate in modo analogo ).
Quindi esiste l'attributo derivato dell'immutabilità (più in generale RT), che le espressioni dichiarative, esprimono solo le relazioni logiche delle parti costituenti (cioè degli argomenti della funzione di sottoespressione) e non le relazioni di stato mutabili .
Ordine di valutazione
La scelta dell'ordine di valutazione delle sottoespressioni può dare un risultato variabile solo quando una qualsiasi delle chiamate di funzione non è RT (ovvero la funzione non è pura), ad esempio si accede a uno stato mutabile esterno a una funzione all'interno della funzione.
Ad esempio, in alcune espressioni nidificate, ad esempio f( g(a, b), h(c, d) )
, la valutazione ansioso e pigro degli argomenti funzione emette gli stessi risultati se le funzioni f
, g
e h
sono puri.
Considerando che, se le funzioni f
, g
e h
non sono pure, la scelta dell'ordine di valutazione può dare un risultato diverso.
Nota, le espressioni nidificate sono funzioni concettualmente nidificate, poiché gli operatori di espressioni sono solo chiamate di funzione mascherate da prefisso unario, postfisso unario o notazione infettiva binaria.
Tangenzialmente, se tutti gli identificatori, ad esempio a
, b
, c
, d
, sono immutabili ovunque, stato esterno per il programma non può essere letta (per esempio I / O), e non v'è nessuna rottura livello di astrazione, quindi funzioni sono sempre pura.
Tra l'altro, Haskell ha una sintassi diversa, f (g a b) (h c d)
.
Dettagli dell'ordine di valutazione
Una funzione è una transizione di stato (non un valore memorizzato mutabile) dall'ingresso all'uscita. Per le composizioni RT di chiamate a funzioni pure , l'ordine di esecuzione di queste transizioni di stato è indipendente. La transizione di stato di ciascuna chiamata di funzione è indipendente dalle altre, a causa della mancanza di effetti collaterali e del principio secondo cui una funzione RT può essere sostituita dal suo valore memorizzato nella cache . Per correggere un malinteso popolare , la pura composizione monadica è sempre dichiarativa e RT , nonostante il fatto che la IO
monade di Haskell sia discutibilmente impura e quindi imperativa contenga lo World
stato esterno al programma (ma nel senso della precisazione sottostante, gli effetti collaterali sono isolati).
Valutazione desiderosa significa che gli argomenti delle funzioni vengono valutati prima che la funzione venga chiamata, e la valutazione pigra significa che gli argomenti non vengono valutati fino a quando (e se) si accede all'interno della funzione.
Definizione : i parametri della funzione sono dichiarati nel sito della definizione della funzione e gli argomenti della funzione sono forniti nel sito della chiamata della funzione . Conosci la differenza tra parametro e argomento .
Concettualmente, tutte le espressioni sono (una composizione di) chiamate di funzione, ad es. Le costanti sono funzioni senza input, gli operatori unari sono funzioni con un input, gli operatori di infissione binaria sono funzioni con due input, i costruttori sono funzioni e persino le istruzioni di controllo (ad es if
. for
, while
) può essere modellato con funzioni. L' ordine che questi argomenti funzioni (da non confondere con ordine chiamata di funzione annidata) vengono valutati non è dichiarato dalla sintassi, ad esempio, f( g() )
potrebbe avidamente valutare g
poi f
su g
's risultato o potrebbe valutare f
e solo pigramente valutare g
se il suo risultato è necessaria all'interno f
.
Un avvertimento, nessun linguaggio completo di Turing (cioè che consente la ricorsione illimitata) è perfettamente dichiarativo, ad esempio una valutazione pigra introduce memoria e indeterminismo temporale. Ma questi effetti collaterali dovuti alla scelta dell'ordine di valutazione sono limitati al consumo di memoria, al tempo di esecuzione, alla latenza, alla non terminazione e all'isteresi esterna, quindi alla sincronizzazione esterna.
Programmazione funzionale
Poiché la programmazione dichiarativa non può avere loop, l'unico modo per iterare è la ricorsione funzionale. È in questo senso che la programmazione funzionale è collegata alla programmazione dichiarativa.
Ma la programmazione funzionale non si limita alla programmazione dichiarativa . La composizione funzionale può essere contrastata con il sottotipo , in particolare per quanto riguarda il Problema di espressione , in cui l'estensione può essere ottenuta aggiungendo sottotipi o decomposizione funzionale . L'estensione può essere un mix di entrambe le metodologie.
La programmazione funzionale di solito rende la funzione un oggetto di prima classe, il che significa che il tipo di funzione può apparire nella grammatica ovunque qualsiasi altro tipo possa. Il risultato è che le funzioni possono inserire e operare su funzioni, fornendo così la separazione delle preoccupazioni enfatizzando la composizione delle funzioni, cioè separando le dipendenze tra le sottocomputer di un calcolo deterministico.
Ad esempio, invece di scrivere una funzione separata (e impiegare la ricorsione invece di loop se la funzione deve anche essere dichiarativa) per ciascuno di un numero infinito di possibili azioni specializzate che potrebbero essere applicate a ciascun elemento di una raccolta, la programmazione funzionale impiega iterazione riutilizzabile funzioni, ad esempio map
, fold
, filter
. Queste funzioni di iterazione introducono una funzione di azione specializzata di prima classe. Queste funzioni di iterazione ripetono la raccolta e richiamano la funzione di azione specializzata input per ciascun elemento. Queste funzioni di azione sono più concise perché non devono più contenere le istruzioni di ciclo per iterare la raccolta.
Tuttavia, nota che se una funzione non è pura, allora è davvero una procedura. Possiamo forse sostenere che la programmazione funzionale che utilizza funzioni impure è in realtà una programmazione procedurale. Pertanto, se concordiamo che le espressioni dichiarative sono RT, allora possiamo dire che la programmazione procedurale non è programmazione dichiarativa, e quindi potremmo sostenere che la programmazione funzionale è sempre RT e deve essere un sottoinsieme della programmazione dichiarativa.
Parallelismo
Questa composizione funzionale con funzioni di prima classe può esprimere la profondità del parallelismo separando la funzione indipendente.
Principio di Brent: il calcolo con lavoro w e profondità d può essere implementato in una PRAM con processore p nel tempo O (max (w / p, d)).
Sia la concorrenza che il parallelismo richiedono anche una programmazione dichiarativa , ovvero immutabilità e RT.
Quindi da dove nasce questa pericolosa ipotesi che il parallelismo == concorrenza? È una conseguenza naturale delle lingue con effetti collaterali: quando la tua lingua ha effetti collaterali ovunque, allora ogni volta che provi a fare più di una cosa alla volta hai essenzialmente un non determinismo causato dall'interlacciamento degli effetti di ogni operazione . Quindi, nei linguaggi con effetti collaterali, l'unico modo per ottenere il parallelismo è la concorrenza; non è quindi sorprendente che spesso vediamo i due confusi.
Si noti che l'ordine di valutazione influisce anche sulla terminazione e sugli effetti collaterali delle prestazioni della composizione funzionale.
Eager (CBV) e lazy (CBN) sono duelli categorici [ 10 ], perché hanno un ordine di valutazione invertito, cioè se le funzioni esterne o interne rispettivamente vengono valutate per prime. Immagina un albero capovolto, quindi desideroso valuta dalle punte del ramo dell'albero delle funzioni fino alla gerarchia dei rami fino al tronco delle funzioni di livello superiore; mentre pigro valuta dal tronco fino alle punte del ramo. Eager non ha prodotti congiuntivi ("e", a / k / a "prodotti" categorici) e pigro non ha coprodotti disgiuntivi ("o", a / k / a "somme" categoriche) [ 11 ].
Prestazione
desideroso
Come per la non terminazione, l'entusiasmo è troppo impaziente di una composizione funzionale congiuntiva, cioè la struttura di controllo compositiva fa un lavoro inutile che non viene fatto con pigro. Ad esempio , desideroso e inutilmente mappa l'intera lista su booleani, quando è composta da una piega che termina sul primo vero elemento.
Questo lavoro non necessario è la causa del preteso "fino a" un ulteriore log n fattore nella complessità temporale sequenziale di desideroso contro pigro, entrambi con funzioni pure. Una soluzione consiste nell'utilizzare i funzione (ad es. Elenchi) con costruttori pigri (ovvero desiderosi di prodotti pigri opzionali), poiché con l'entusiasmo l'erroneità dell'erroneità deriva dalla funzione interna. Questo perché i prodotti sono tipi costruttivi, ovvero tipi induttivi con un'algebra iniziale su un punto di fissaggio iniziale [ 11 ]
Pigro
Come per la non terminazione, il pigro è troppo pigro con una composizione funzionale disgiuntiva, vale a dire che la finalità coinduttiva può avvenire più tardi del necessario, risultando in un lavoro non necessario e nel non determinismo del ritardo che non è il caso di desideroso [ 10 ] [ 11 ] . Esempi di finalità sono le eccezioni di stato, temporizzazione, non terminazione ed esecuzione. Questi sono effetti collaterali imperativi, ma anche in un linguaggio dichiarativo puro (ad es. Haskell), c'è uno stato nell'imperativo IO monade (nota: non tutte le monadi sono imperative!) Implicito nell'allocazione dello spazio, e il tempismo è stato relativo all'imperativo mondo reale. L'uso di pigro anche con coprodotti desiderosi opzionali perde "pigrizia" nei coprodotti interni, perché con pigro la pigrizia errata ha origine dalla funzione esterna(vedere l'esempio nella sezione Non-termination, dove == è una funzione dell'operatore binario esterno). Questo perché i coprodotti sono limitati dalla finalità, cioè tipi coinduttivi con un'algebra finale su un oggetto finale [ 11 ].
Lazy causa indeterminismo nella progettazione e nel debug di funzioni per latenza e spazio, il cui debug è probabilmente al di là delle capacità della maggior parte dei programmatori, a causa della dissonanza tra la gerarchia di funzioni dichiarata e l'ordine di valutazione del runtime. Le funzioni pure pigre, valutate con entusiasmo, potrebbero potenzialmente introdurre una non terminazione mai vista prima durante l'esecuzione. Al contrario, le funzioni pure desiderose valutate con lazy, potrebbero potenzialmente introdurre indeterminismo di spazio e latenza mai visto prima durante l'esecuzione.
Non-terminazione
In fase di compilazione, a causa del problema di Halting e della ricorsione reciproca in un linguaggio completo di Turing, non è possibile garantire in genere che le funzioni vengano interrotte.
desideroso
Con desideroso ma non pigro, per la congiunzione di Head
"e" Tail
, se uno dei due Head
o Tail
non termina, rispettivamente rispettivamente List( Head(), Tail() ).tail == Tail()
o List( Head(), Tail() ).head == Head()
non è vero perché il lato sinistro non lo fa e il lato destro termina.
Considerando che con pigro terminano entrambe le parti. Così desideroso è troppo desideroso di prodotti congiuntivi e non termina (comprese le eccezioni di runtime) nei casi in cui non è necessario.
Pigro
Con pigro ma non desideroso, per la disgiunzione di 1
"o" 2
, se f
non termina, allora List( f ? 1 : 2, 3 ).tail == (f ? List( 1, 3 ) : List( 2, 3 )).tail
non è vero perché termina il lato sinistro e il lato destro no.
Considerando che, con impaziente nessuna delle due parti termina, il test di uguaglianza non viene mai raggiunto. Quindi pigro è troppo pigro con coprodotti disgiuntivi, e in quei casi non riesce a terminare (comprese le eccezioni di runtime) dopo aver fatto più lavoro di quanto desiderasse.
[ 10 ] Continuazioni dichiarative e dualità categorica, Filinski, sezioni 2.5.4 Un confronto tra CBV e CBN e 3.6.1 CBV e CBN nella SCL.
[ 11 ] Continuazioni dichiarative e dualità categorica, Filinski, sezioni 2.2.1 Prodotti e coprodotti, 2.2.2 Oggetti terminali e iniziali, 2.5.2 CBV con prodotti pigri e 2.5.3 CBN con coprodotti desiderosi.
Non esiste una definizione oggettiva non ambigua per questi. Ecco come io li definirei:
Imperativo - Il focus è su ciò che passi il computer dovrebbe prendere piuttosto che ciò che il computer fare (ex C, C ++, Java.).
Dichiarativo - L'attenzione si concentra su ciò che il computer dovrebbe fare piuttosto che su come dovrebbe farlo (es. SQL).
Funzionale - un sottoinsieme di linguaggi dichiarativi che si concentra fortemente sulla ricorsione
imperativo e dichiarativo descrivono due stili opposti di programmazione. l'imperativo è l'approccio tradizionale "ricetta passo per passo" mentre il dichiarativo è più "questo è quello che voglio, ora capisci come farlo".
questi due approcci si verificano durante la programmazione, anche con lo stesso linguaggio e lo stesso programma. generalmente l'approccio dichiarativo è considerato preferibile, perché libera il programmatore dal dover specificare così tanti dettagli, pur avendo meno possibilità di bug (se descrivi il risultato che desideri, e alcuni processi automatici ben collaudati possono tornare indietro da quello a definire i passaggi, quindi si potrebbe sperare che le cose siano più affidabili rispetto alla necessità di specificare ogni passaggio manualmente).
d'altra parte, un approccio imperativo ti dà un controllo di livello più basso: è l '"approccio micromanager" alla programmazione. e ciò può consentire al programmatore di sfruttare la conoscenza del problema per fornire una risposta più efficiente. quindi non è insolito che alcune parti di un programma siano scritte in uno stile più dichiarativo, ma che le parti critiche per la velocità siano più imperative.
come puoi immaginare, la lingua che usi per scrivere un programma influenza quanto puoi essere dichiarativo - una lingua che ha "intelligenze" incorporate per capire cosa fare data una descrizione del risultato consentirà una dichiarazione molto più dichiarativa approccio rispetto a quello in cui il programmatore deve prima aggiungere quel tipo di intelligenza con codice imperativo prima di poter costruire un livello più dichiarativo in cima. quindi, ad esempio, un linguaggio come prolog è considerato molto dichiarativo perché ha un processo incorporato che cerca le risposte.
finora, noterai che non ho menzionato la programmazione funzionale . questo perché è un termine il cui significato non è immediatamente correlato agli altri due. nella sua programmazione più semplice e funzionale significa che usi le funzioni. in particolare, che si utilizza un linguaggio che supporta le funzioni come "valori di prima classe" - ciò significa che non solo è possibile scrivere funzioni, ma è possibile scrivere funzioni che scrivono funzioni (che scrivono funzioni che ...) e passare funzioni a funzioni. in breve: le funzioni sono flessibili e comuni come cose come stringhe e numeri.
potrebbe sembrare strano, quindi, che funzionale, imperativo e dichiarativo siano spesso citati insieme. la ragione di ciò è una conseguenza dell'idea "estrema" dell'idea della programmazione funzionale. una funzione, nel suo senso più puro, è qualcosa di matematico - una specie di "scatola nera" che accetta alcuni input e fornisce sempre lo stesso output. e quel tipo di comportamento non richiede la memorizzazione di variabili variabili. quindi se si progetta un linguaggio di programmazione il cui scopo è quello di implementare un'idea di programmazione funzionale molto pura e influenzata matematicamente, si finisce per rifiutare, in gran parte, l'idea di valori che possono cambiare (in un certo senso limitato, dal punto di vista tecnico).
e se lo fai - se limiti il modo in cui le variabili possono cambiare - allora quasi per caso finisci per forzare il programmatore a scrivere programmi che sono più dichiarativi, perché gran parte della programmazione imperativa sta descrivendo come cambiano le variabili e non puoi più Fai quello! così risulta che la programmazione funzionale - in particolare la programmazione in un linguaggio funzionale - tende a dare un codice più dichiarativo.
per riassumere, quindi:
imperativo e dichiarativo sono due stili opposti di programmazione (gli stessi nomi sono usati per linguaggi di programmazione che incoraggiano quegli stili)
la programmazione funzionale è uno stile di programmazione in cui le funzioni diventano molto importanti e, di conseguenza, la modifica dei valori diventa meno importante. la limitata capacità di specificare cambiamenti nei valori impone uno stile più dichiarativo.
così la "programmazione funzionale" è spesso descritta come "dichiarativa".
In breve:
Un linguaggio imperativo specifica una serie di istruzioni che il computer esegue in sequenza (fare questo, quindi farlo).
Un linguaggio dichiarativo dichiara un insieme di regole su quali output dovrebbero derivare da quali input (es. Se hai A, allora il risultato è B). Un motore applicherà queste regole agli input e fornirà un output.
Un linguaggio funzionale dichiara un insieme di funzioni matematiche / logiche che definiscono il modo in cui l'input viene tradotto in output. per esempio. f (y) = y * y. è un tipo di linguaggio dichiarativo.
Imperativo: come raggiungere il nostro obiettivo
Take the next customer from a list.
If the customer lives in Spain, show their details.
If there are more customers in the list, go to the beginning
Dichiarativo: cosa vogliamo ottenere
Show customer details of every customer living in Spain
Programmazione imperativa indica qualsiasi tipo di programmazione in cui il programma è strutturato in base alle istruzioni che descrivono come verranno eseguite le operazioni eseguite da un computer .
Programmazione dichiarativa indica qualsiasi stile di programmazione in cui il programma è una descrizione del problema o della soluzione, ma non specifica esplicitamente come verrà svolto il lavoro .
La programmazione funzionale sta programmando valutando le funzioni e le funzioni delle funzioni ... Poiché la programmazione funzionale (rigorosamente definita) significa programmare definendo funzioni matematiche libere di effetti collaterali, quindi è una forma di programmazione dichiarativa ma non è l'unico tipo di programmazione dichiarativa .
Programmazione logica (ad esempio in Prolog) è un'altra forma di programmazione dichiarativa. Implica il calcolo decidendo se un'affermazione logica è vera (o se può essere soddisfatta). Il programma è in genere una serie di fatti e regole, vale a dire una descrizione piuttosto che una serie di istruzioni.
La riscrittura dei termini (ad esempio CASL) è un'altra forma di programmazione dichiarativa. Implica la trasformazione simbolica di termini algebrici. È completamente distinto dalla programmazione logica e dalla programmazione funzionale.
imperativo : le espressioni descrivono la sequenza di azioni da eseguire (associativa)
dichiarativo - le espressioni sono dichiarazioni che contribuiscono al comportamento del programma (associativo, commutativo, idempotente, monotonico)
funzionale : le espressioni hanno valore come unico effetto; la semantica supporta il ragionamento equazionale
Da quando ho scritto la mia risposta precedente, ho formulato una nuova definizione della proprietà dichiarativa che è citata di seguito. Ho anche definito la programmazione imperativa come doppia proprietà.
Questa definizione è superiore a quella che ho fornito nella mia risposta precedente, perché è concisa ed è più generale. Ma può essere più difficile brontolare, perché le implicazioni dei teoremi di incompletezza applicabili alla programmazione e alla vita in generale sono difficili per gli esseri umani che si muovono intorno.
La spiegazione citata della definizione discute il ruolo della pura programmazione funzionale nella programmazione dichiarativa.
Tutti i tipi di programmazione esotici si adattano alla seguente tassonomia di dichiarativo contro imperativo, poiché la seguente definizione afferma che sono doppi.
Dichiarativo vs. Imperativo
La proprietà dichiarativa è strana, ottusa e difficile da catturare in una definizione tecnicamente precisa che rimane generale e non ambigua, perché è un'idea ingenua che possiamo dichiarare il significato (alias semantica) del programma senza incorrere in effetti collaterali non intenzionali. Esiste una tensione intrinseca tra espressione di significato ed evitamento di effetti indesiderati, e questa tensione deriva in realtà dai teoremi di incompletezza della programmazione e del nostro universo.
È una semplificazione eccessiva, tecnicamente imprecisa e spesso ambigua definire dichiarativo come " cosa fare " e imperativo come " come fare " . Un caso ambiguo è il " cosa " è il " come " in un programma che genera un programma, un compilatore.
Evidentemente la ricorsione illimitata che completa un linguaggio di Turing , è anche analoga nella semantica, non solo nella struttura sintattica della valutazione (nota anche come semantica operativa). Questo è logicamente un esempio analogo al teorema di Gödel: “ qualsiasi sistema completo di assiomi è anche incoerente ". Rifletti sulla stranezza contraddittoria di quella citazione! È anche un esempio che dimostra come l'espressione della semantica non abbia un limite dimostrabile, quindi non possiamo provare 2 che un programma (e analogamente la sua semantica) fermi, noto anche come teorema di Halting.
I teoremi di incompletezza derivano dalla natura fondamentale del nostro universo, che come affermato nella Seconda Legge della Termodinamica è " l'entropia (ovvero il numero di possibilità indipendenti) tende al massimo per sempre ". La codifica e la progettazione di un programma non è mai finita - è viva! - perché tenta di soddisfare un'esigenza del mondo reale e la semantica del mondo reale è in continua evoluzione e tende a più possibilità. Gli umani non smettono mai di scoprire cose nuove (inclusi errori nei programmi ;-).
Per catturare con precisione e tecnicamente questa nozione desiderata di cui sopra in questo strano universo che non ha margini (meditare che! Non esiste un "esterno" del nostro universo), richiede una definizione concisa ma ingannevolmente non semplice che suonerà errata fino a quando non viene spiegata profondamente.
Definizione:
La proprietà dichiarativa è dove esiste una sola serie di istruzioni che può esprimere ogni semantica modulare specifica.
La proprietà imperativa 3 è la doppia, in cui la semantica è incoerente sotto la composizione e / o può essere espressa con variazioni di insiemi di affermazioni.
Questa definizione di dichiarativo è distintamente locale nell'ambito semantico, nel senso che richiede che un semantico modulare mantenga il suo significato coerente indipendentemente da dove e come viene istanziato e impiegato nell'ambito globale . Quindi ogni semantico modulare dichiarativo dovrebbe essere intrinsecamente ortogonale a tutti gli altri possibili - e non un algoritmo o modello globale impossibile (a causa di teoremi di incompletezza) per testimoniare la coerenza, che è anche il punto di “ More Is Not Always Better " di Robert Harper, professore di Informatica alla Carnegie Mellon University, uno dei progettisti di Standard ML.
Esempi di queste semantiche dichiarative modulari includono i teorici di categoria, ad esempio i
Applicative
tipizzazione nominale, gli spazi dei nomi, i campi nominati e la scrittura a livello operativo della semantica, quindi la pura programmazione funzionale.Quindi linguaggi dichiarativi ben progettati possono esprimere più chiaramente il significato , anche se con una certa perdita di generalità in ciò che può essere espresso, eppure un guadagno in ciò che può essere espresso con coerenza intrinseca.
Un esempio della suddetta definizione è l'insieme di formule nelle celle di un programma per fogli di calcolo, che non dovrebbero avere lo stesso significato quando vengono spostate in celle di colonne e righe diverse, vale a dire che gli identificatori di celle vengono modificati. Gli identificatori cellulari fanno parte e non superano il significato previsto. Quindi ogni risultato del foglio di calcolo è univoco rispetto agli identificatori di cella in una serie di formule. La semantica modulare coerente in questo caso è l'uso di identificatori di celle come input e output di funzioni pure per le formule di celle (vedi sotto).
Hyper Text Markup Language, noto anche come HTML, il linguaggio delle pagine Web statiche, è un esempio di un linguaggio dichiarativo altamente (ma non perfettamente 3 ) che (almeno prima di HTML 5) non era in grado di esprimere un comportamento dinamico. L'HTML è forse il linguaggio più semplice da imparare. Per un comportamento dinamico, un linguaggio di scripting imperativo come JavaScript era di solito combinato con HTML. L'HTML senza JavaScript si adatta alla definizione dichiarativa perché ogni tipo nominale (ovvero i tag) mantiene il suo significato coerente sotto la composizione all'interno delle regole della sintassi.
Una definizione concorrente per dichiarativo è la proprietà commutativa e idempotente delle dichiarazioni semantiche, cioè che le dichiarazioni possono essere riordinate e duplicate senza cambiarne il significato. Ad esempio, le istruzioni che assegnano valori ai campi con nome possono essere riordinate e duplicate senza modificare il significato del programma, se tali nomi sono modulari scritti in un ordine implicito. I nomi a volte implicano un ordine, ad esempio gli identificatori di cella includono la posizione di colonna e riga: lo spostamento di un totale sul foglio di calcolo ne modifica il significato. Altrimenti, queste proprietà implicitamente richiedono globalecoerenza della semantica. È generalmente impossibile progettare la semantica delle dichiarazioni in modo che rimangano coerenti se ordinate o duplicate in modo casuale, poiché l'ordine e la duplicazione sono intrinseci alla semantica. Ad esempio, le dichiarazioni "Foo esiste" (o costruzione) e "Foo non esiste" (e distruzione). Se si considera un'incoerenza casuale endemica della semantica prevista, si accetta questa definizione come abbastanza generale per la proprietà dichiarativa. In sostanza questa definizione è vacua come definizione generalizzata perché cerca di rendere la coerenza ortogonale alla semantica, cioè di sfidare il fatto che l'universo della semantica è dinamicamente illimitato e non può essere catturato in un paradigma di coerenza globale .
Richiedere le proprietà commutative e idempotenti per la (semantica di valutazione strutturale della) semantica operativa di livello inferiore converte la semantica operativa in una semantica modulare localizzata dichiarativa , ad esempio pura programmazione funzionale (inclusa la ricorsione invece di loop imperativi). Quindi l'ordine operativo dei dettagli di implementazione non influisce (cioè si diffonde a livello globale ) sulla coerenza della semantica di livello superiore. Ad esempio, l'ordine di valutazione (e teoricamente anche la duplicazione) delle formule del foglio di calcolo non ha importanza perché gli output non vengono copiati negli input fino a quando non sono stati calcolati tutti gli output, vale a dire analoghi alle funzioni pure.
C, Java, C ++, C #, PHP e JavaScript non sono particolarmente dichiarativi. La sintassi di Copute e la sintassi di Python sono accoppiate in modo più dichiarativo ai risultati previsti , ovvero semantica sintattica coerente che elimina l'estraneo in modo da poter comprendere facilmente il codice dopo averlo dimenticato. Copute e Haskell applicano il determinismo della semantica operativa e incoraggiano " non ripetere te stesso " (DRY), perché consentono solo il puro paradigma funzionale.
2 Anche dove possiamo dimostrare la semantica di un programma, ad esempio con la lingua Coq, questo è limitato alla semantica espressa nella digitazione e la digitazione non può mai catturare tutta la semantica di un programma, nemmeno per le lingue che sono non completo di Turing, ad es. con HTML + CSS è possibile esprimere combinazioni incoerenti che hanno quindi una semantica indefinita.
3 Molte spiegazioni affermano erroneamente che solo la programmazione imperativa ha istruzioni sintatticamente ordinate. Ho chiarito questa confusione tra programmazione imperativa e funzionale . Ad esempio, l'ordine delle istruzioni HTML non riduce la coerenza del loro significato.
Modifica: ho pubblicato il seguente commento sul blog di Robert Harper:
nella programmazione funzionale ... l'intervallo di variazione di una variabile è un tipo
A seconda di come si distingue la programmazione funzionale dalla programmazione imperativa, il proprio 'assegnabile' in un programma imperativo può anche avere un tipo che pone un limite alla sua variabilità.
L'unica definizione non confusa che attualmente apprezzo per la programmazione funzionale è a) funzioni come oggetti e tipi di prima classe, b) preferenza per la ricorsione su loop e / o c) funzioni pure - cioè quelle funzioni che non influiscono sulla semantica desiderata del programma quando memorizzato ( quindi una programmazione funzionale perfettamente pura non esiste in una semantica denotazionale per scopi generali a causa degli impatti della semantica operativa, ad es. allocazione della memoria ).
La proprietà idempotente di una funzione pura significa che la chiamata di funzione sulle sue variabili può essere sostituita dal suo valore, il che non è generalmente il caso degli argomenti di una procedura imperativa. Le funzioni pure sembrano essere dichiarative rispetto alle transizioni di stato non poste tra i tipi di input e di risultato.
Ma la composizione di funzioni pure non mantiene tale coerenza, poiché è possibile modellare un processo imperativo di effetti collaterali (stato globale) in un linguaggio di programmazione funzionale puro, ad esempio IOMonad di Haskell e inoltre è del tutto impossibile impedire di farlo in qualsiasi linguaggio di programmazione funzionale puro completo di Turing.
Come ho scritto nel 2012, che sembra al consenso simile dei commenti nel tuo recente blog , che la programmazione dichiarativa è un tentativo di catturare l'idea che la semantica prevista non è mai opaca. Esempi di semantica opaca sono la dipendenza dall'ordine, la dipendenza dalla cancellazione della semantica di livello superiore a livello di semantica operativa (ad es. I cast non sono conversioni e la generica reificata limita la semantica di livello superiore ) e la dipendenza da valori variabili che non possono essere verificati (dimostrato corretto) dal linguaggio di programmazione.
Pertanto, ho concluso che solo le lingue complete non turing possono essere dichiarative.
Pertanto, un attributo non ambiguo e distinto di un linguaggio dichiarativo potrebbe essere che la sua produzione può essere dimostrata obbedire a un insieme numeroso di regole generative. Ad esempio, per qualsiasi programma HTML specifico (ignorando le differenze nel modo in cui gli interpreti divergono) che non è scritto (ovvero non è Turing completo), la sua variabilità di output può essere enumerabile. O più succintamente un programma HTML è una pura funzione della sua variabilità. Idem un programma per fogli di calcolo è una pura funzione delle sue variabili di input.
Quindi mi sembra che le lingue dichiarative siano l'antitesi della ricorsione illimitata , cioè secondo il secondo teorema di incompletezza di Gödel i teoremi autoreferenziali non possono essere provati.
Lesie Lamport ha scritto una fiaba su come Euclide avrebbe potuto aggirare i teoremi di incompletezza di Gödel applicati alle prove matematiche nel contesto del linguaggio di programmazione mediante la congruenza tra tipi e logica (corrispondenza Curry-Howard, ecc.).
Programmazione imperativa: dire alla "macchina" come fare qualcosa e, di conseguenza, accadrà quello che vuoi che accada.
Programmazione dichiarativa: dire alla "macchina" cosa vorresti succedere e lasciare che il computer capisse come farlo.
function makeWidget(options) {
const element = document.createElement('div');
element.style.backgroundColor = options.bgColor;
element.style.width = options.width;
element.style.height = options.height;
element.textContent = options.txt;
return element;
}
function makeWidget(type, txt) {
return new Element(type, txt);
}
Nota: la differenza non è di brevità, complessità o astrazione. Come detto, la differenza è come vs cosa .
Gli aspetti Imperativo / Dichiarativo / Funzionale erano buoni in passato per classificare i linguaggi generici, ma oggigiorno tutti i "grandi linguaggi" (come Java, Python, Javascript, ecc.) Hanno qualche opzione (tipicamente framework ) da esprimere con "altro focus" rispetto al suo principale (solito imperativo) e per esprimere processi paralleli, funzioni dichiarative, lambda, ecc.
Quindi una buona variante di questa domanda è "Quale aspetto è buono classificare i framework oggi?" ... Un aspetto importante è qualcosa che possiamo etichettare "stile di programmazione" ...
Un buon esempio da spiegare. Come puoi leggere su jQuery su Wikipedia ,
L'insieme delle funzionalità principali di jQuery - selezione, spostamento e manipolazione di elementi DOM -, abilitato dal suo motore di selezione (...), ha creato un nuovo "stile di programmazione", fondendo algoritmi e strutture di dati DOM
Quindi jQuery è il miglior esempio (popolare) di concentrarsi su un "nuovo stile di programmazione" , che non è solo l'orientamento agli oggetti, è " Fondere algoritmi e strutture di dati ". jQuery è in qualche modo reattivo come fogli di calcolo, ma non "orientato alle celle", è " orientato al nodo DOM " ... Confrontando gli stili principali in questo contesto:
Nessuna fusione : in tutti i "grandi linguaggi", in qualsiasi espressione Funzionale / Dichiarativa / Imperativa, il solito è "nessuna fusione" di dati e algoritmo, tranne che per qualche orientamento all'oggetto, che è una fusione nel punto di vista della struttura algebrica rigorosa .
Un po 'di fusione : tutte le classiche strategie di fusione, al giorno d'oggi hanno un po' di framework che lo utilizza come paradigma ... flusso di dati , programmazione basata su eventi (o vecchi linguaggi specifici di dominio come awk e XSLT ) ... Come la programmazione con i moderni fogli di calcolo, sono anche esempi di stile di programmazione reattiva .
Grande fusione : è "lo stile jQuery" ... jQuery è un linguaggio specifico di dominio incentrato su " fusione di algoritmi e strutture di dati DOM ".
PS: altri "linguaggi di query", come XQuery, SQL (con PL come opzione di espressione imperativa) sono anche esempi di fusione di algoritmi di dati, ma sono isole , senza fusione con altri moduli di sistema ... Primavera , quando si usa find()
-variants e clausole di specifica , è un altro buon esempio di fusione.
La programmazione dichiarativa sta programmando esprimendo una logica senza tempo tra l'input e l'output, ad esempio in pseudocodice, il seguente esempio sarebbe dichiarativo:
def factorial(n):
if n < 2:
return 1
else:
return factorial(n-1)
output = factorial(argvec[0])
Qui definiamo semplicemente una relazione chiamata "fattoriale" e definiamo la relazione tra l'output e l'input come quella relazione. Come dovrebbe essere evidente qui, qualsiasi linguaggio strutturato consente in qualche modo la programmazione dichiarativa. Un'idea centrale della programmazione dichiarativa sono i dati immutabili, se si assegna a una variabile, lo si fa solo una volta e poi mai più. Altre definizioni più rigorose implicano che potrebbero non esserci effetti collaterali, queste lingue sono talvolta chiamate "puramente dichiarative".
Lo stesso risultato in uno stile imperativo sarebbe:
a = 1
b = argvec[0]
while(b < 2):
a * b--
output = a
In questo esempio, non abbiamo espresso alcuna relazione logica statica senza tempo tra l'input e l'output, abbiamo cambiato manualmente gli indirizzi di memoria fino a quando uno di loro ha ottenuto il risultato desiderato. Dovrebbe essere evidente che tutte le lingue permettono alla semantica dichiarativa di estendersi, ma non tutte consentono imperativo, alcune lingue dichiarative "puramente" consentono del tutto effetti collaterali e mutazione.
Si dice spesso che le lingue dichiarative specificano "cosa deve essere fatto", al contrario di "come farlo", penso che sia un termine improprio, i programmi dichiarativi specificano ancora come si debba ottenere da input a output, ma in un altro modo, il la relazione che specifichi deve essere effettivamente calcolabile (termine importante, cercalo se non lo conosci). Un altro approccio è la programmazione non deterministica , che specifica in realtà solo quali condizioni soddisfano un risultato, prima che l'implementazione finisca per esaurire tutti i percorsi in prova ed errore fino a quando non ha successo.
Le lingue puramente dichiarative includono Haskell e Pure Prolog. Una scala mobile dall'una all'altra sarebbe: Pure Prolog, Haskell, OCaml, Scheme / Lisp, Python, Javascript, C--, Perl, PHP, C ++, Pascall, C, Fortran, Assembly
factorial
non muta alcun valore.
Alcune buone risposte qui per quanto riguarda i "tipi" noti.
Propongo alcuni concetti aggiuntivi, più "esotici", spesso associati alla folla della programmazione funzionale:
Penso che la tua tassonomia non sia corretta. Esistono due tipi opposti: imperativo e dichiarativo. Funzionale è solo un sottotipo di dichiarativo. A proposito, Wikipedia afferma lo stesso fatto.
In breve, più uno stile di programmazione enfatizza il Cosa (da fare) sottraendo i dettagli di Come (da fare) più tale stile è considerato dichiarativo. È vero il contrario per l'imperativo. La programmazione funzionale è associata allo stile dichiarativo.