Cosa rende dichiarativi i linguaggi di programmazione funzionale rispetto a Imperativo?


17

In molti articoli, che descrivono i vantaggi della programmazione funzionale, ho visto linguaggi di programmazione funzionale, come Haskell, ML, Scala o Clojure, definiti "linguaggi dichiarativi" distinti da linguaggi imperativi come C / C ++ / C # / Java. La mia domanda è cosa rende i linguaggi di programmazione funzionale dichiarativi anziché imperativi.

Una spiegazione spesso riscontrata che descrive le differenze tra la programmazione dichiarativa e quella imperativa è che nella programmazione imperativa si dice al computer "Come fare qualcosa" anziché "Cosa fare" nei linguaggi dichiarativi. Il problema che ho con questa spiegazione è che stai facendo costantemente entrambi in tutti i linguaggi di programmazione. Anche se si scende all'assemblaggio di livello più basso, si sta ancora dicendo al computer "Cosa fare", si dice alla CPU di aggiungere due numeri, non si istruisce su come eseguire l'aggiunta. Se andiamo dall'altra parte dello spettro, un linguaggio funzionale puro di alto livello come Haskell, in realtà stai dicendo al computer come realizzare un compito specifico, questo è ciò che il tuo programma è una sequenza di istruzioni per realizzare un compito specifico che il computer non sa come realizzare da solo. Comprendo che linguaggi come Haskell, Clojure, ecc. Sono ovviamente di livello superiore rispetto a C / C ++ / C # / Java e offrono funzionalità come valutazione pigra, strutture di dati immutabili, funzioni anonime, curry, strutture di dati persistenti, ecc. Tutto ciò che rende programmazione funzionale possibile ed efficiente, ma non li classificherei come linguaggi dichiarativi.

Un linguaggio dichiarativo puro per me sarebbe uno che è composto esclusivamente da dichiarazioni, un esempio di tali linguaggi sarebbe CSS (Sì, lo so che CSS non sarebbe tecnicamente un linguaggio di programmazione). CSS contiene solo dichiarazioni di stile utilizzate da HTML e Javascript della pagina. Il CSS non può fare nient'altro oltre a fare dichiarazioni, non può creare funzioni di classe, ovvero funzioni che determinano lo stile da visualizzare in base ad alcuni parametri, non è possibile eseguire script CSS, ecc. Che per me descrive un linguaggio dichiarativo (nota che non ho detto dichiarativo linguaggio di programmazione ).

Aggiornare:

Ho giocato con Prolog di recente e per me Prolog è il linguaggio di programmazione più vicino a un linguaggio pienamente dichiarativo (almeno secondo me), se non è l'unico linguaggio di programmazione pienamente dichiarativo. Elaborare la programmazione in Prolog è fatto facendo dichiarazioni che affermano o un fatto (una funzione predicata che ritorna vera per un input specifico) o una regola (una funzione predicata che ritorna vera per una data condizione / modello basata sugli input), regole sono definiti usando una tecnica di corrispondenza del modello. Per fare qualsiasi cosa in prolog, è necessario interrogare la knowledge base sostituendo uno o più input di un predicato con una variabile e prolog cerca di trovare valori per le variabili per cui il predicato ha esito positivo.

Il mio punto è nel prologo: non ci sono istruzioni imperative, in sostanza stai dicendo (dichiarando) al computer ciò che sa e poi chiedendo (interrogando) sulla conoscenza. Nei linguaggi di programmazione funzionale stai ancora dando istruzioni, cioè prendi un valore, chiama la funzione X e aggiungi 1 ad essa, ecc., Anche se non stai manipolando direttamente le posizioni di memoria o scrivendo il calcolo passo dopo passo. Non direi che la programmazione in Haskell, ML, Scala o Clojure sia dichiarativa in questo senso, anche se potrei sbagliarmi. È vera, vera, pura programmazione funzionale dichiarativa nel senso che ho descritto sopra.


Dov'è @JimmyHoffa quando hai bisogno di lui?

2
1. C # e C ++ moderno sono linguaggi molto funzionali. 2. Pensa alla differenza di scrivere SQL e Java per raggiungere uno scopo simile (forse una query complessa con join). puoi anche pensare in che modo LINQ in C # differisce dalla scrittura di semplici cicli foreach ...
AK_

anche la differenza è più una questione di stile che qualcosa di chiaramente definito ... a meno che tu non stia usando il prologo ...
AK_

1
Una delle chiavi per riconoscere la differenza è quando si utilizza un operatore di assegnazione , rispetto a un operatore di definizione / dichiarazione - ad esempio in Haskell non esiste un operatore di assegnazione . Il comportamento di questi due operatori è molto diverso e ti costringe a progettare il tuo codice in modo diverso.
Jimmy Hoffa,

1
Sfortunatamente anche senza l'operatore di assegnazione posso farlo (let [x 1] (let [x (+ x 2)] (let [x (* x x)] x)))(spero che tu l'abbia capito, Clojure). La mia domanda originale è ciò che rende questo diverso da questo int x = 1; x += 2; x *= x; return x;Secondo me è principalmente lo stesso.
ALXGTV

Risposte:


16

Sembra che tu stia disegnando una linea tra dichiarare le cose e istruire una macchina. Non esiste una separazione così difficile. Poiché la macchina che viene istruita nella programmazione imperativa non deve necessariamente essere un hardware fisico, c'è molta libertà di interpretazione. Quasi tutto può essere visto come un programma esplicito per la giusta macchina astratta. Ad esempio, è possibile visualizzare CSS come un linguaggio di livello piuttosto elevato per la programmazione di una macchina che risolve principalmente i selettori e imposta gli attributi degli oggetti DOM così selezionati.

La domanda è se tale prospettiva sia sensata e, al contrario, quanto la sequenza di istruzioni assomigli a una dichiarazione del risultato da calcolare. Per i CSS, la prospettiva dichiarativa è chiaramente più utile. Per C, la prospettiva imperativa è chiaramente predominante. Per quanto riguarda le lingue come Haskell, beh ...

La lingua ha specificato, semantica concreta. Ovviamente si può interpretare il programma come una catena di operazioni. Non ci vuole nemmeno troppo sforzo per scegliere le operazioni primitive in modo che si associno perfettamente all'hardware delle materie prime (questo è ciò che fanno la macchina STG e altri modelli).

Tuttavia, il modo in cui i programmi Haskell sono scritti, spesso possono essere letti in modo sensato come una descrizione del risultato da calcolare. Prendiamo, ad esempio, il programma per calcolare la somma dei primi N fattoriali:

sum_of_fac n = sum [product [1..i] | i <- ..n]

Puoi desugar questo e leggerlo come una sequenza di operazioni STG, ma molto più naturale è leggerlo come una descrizione del risultato (che è, penso, una definizione più utile della programmazione dichiarativa di "cosa calcolare"): Il risultato è la somma dei prodotti [1..i]per tutti i= 0,…, n. E questo è molto più dichiarativo di quasi qualsiasi programma o funzione C.


1
Il motivo per cui ho posto questa domanda è che molte persone cercano di promuovere la programmazione funzionale come un nuovo paradigma distinto completamente separato dai paradigmi di C / C ++ / C # / Java o persino Python quando in realtà la programmazione funzionale è solo un'altra forma di livello superiore di programmazione imperativa.
ALXGTV,

Per spiegare cosa intendo qui stai definendo sum_of_fac ma sum_of_fac è praticamente inutile da solo, a meno che non sia quello che fa la tua applicazione hole, devi usare il risultato per calcolare qualche altro risultato che alla fine si costruisce per raggiungere un compito specifico, il compito che il programma è stato progettato per risolvere.
ALXGTV,

6
@ALXGTV La programmazione funzionale è un paradigma molto diverso, anche se sei irremovibile nel leggerlo come imperativo (è una classificazione molto ampia, c'è di più nei paradigmi di programmazione di quello). E l'altro codice che si basa su questo codice può essere letto anche in modo dichiarativo: ad esempio map (sum_of_fac . read) (lines fileContent). Naturalmente a un certo punto l'I / O entra in gioco ma, come ho già detto, è un continuum. Parti dei programmi di Haskell sono più imperative, certo, proprio come C ha una sintassi di espressione sorta-dichiarativa ( x + y, non load x; load y; add;)

1
@ALXGTV La tua intuizione è corretta: la programmazione dichiarativa "solo" fornisce un livello più alto di astrazione su determinati dettagli imperativi. Direi che questo è un grosso problema!
Andres F.

1
@Brendan Non penso che la programmazione funzionale sia un sottoinsieme della programmazione imperativa (né l'immutabilità è un tratto obbligatorio di FP). Si può sostenere che nel caso di FP puro, tipicamente statico, è un superset di programmazione imperativa in cui gli effetti sono espliciti nei tipi. Conosci l'affermazione ironica che Haskell è "la lingua imperativa più bella del mondo";)
Andres F.

21

L'unità di base di un programma imperativo è la dichiarazione . Le dichiarazioni vengono eseguite per i loro effetti collaterali. Modificano lo stato che ricevono. Una sequenza di istruzioni è una sequenza di comandi, a indicare che fare questo e poi farlo. Il programmatore specifica l'ordine esatto per eseguire il calcolo. Questo è ciò che la gente intende dicendo al computer come farlo.

L'unità di base di un programma dichiarativo è l' espressione . Le espressioni non hanno effetti collaterali. Specificano una relazione tra un input e un output, creando un output nuovo e separato dal loro input, anziché modificarne lo stato. Una sequenza di espressioni è insignificante senza alcune espressioni contenitive che specificano la relazione tra loro. Il programmatore specifica le relazioni tra i dati e il programma impartisce l'ordine di eseguire il calcolo da tali relazioni. Questo è ciò che la gente intende dicendo al computer cosa fare.

Le lingue imperative hanno espressioni, ma i loro mezzi principali per ottenere risultati sono le dichiarazioni. Allo stesso modo, i linguaggi dichiarativi hanno alcune espressioni con semantica simili alle affermazioni, come le sequenze di monade all'interno della donotazione di Haskell , ma al loro centro, sono una grande espressione. Ciò consente ai principianti di scrivere un codice dall'aspetto molto imperativo, ma il vero potere del linguaggio arriva quando sfuggite a quel paradigma.


1
Questa sarebbe una risposta perfetta se contenesse un esempio. L'esempio migliore che io conosca per illustrare dichiarativa vs. imperativo è Linq vs. foreach.
Robert Harvey,

7

La vera caratteristica distintiva che separa la programmazione dichiarativa dalla programmazione imperativa è nello stile dichiarativo che non si stanno dando istruzioni sequenziali; al livello inferiore sì, la CPU funziona in questo modo, ma questa è una preoccupazione del compilatore.

Suggerisci che il CSS sia un "linguaggio dichiarativo", non lo definirei affatto un linguaggio. È un formato di struttura di dati, come JSON, XML, CSV o INI, solo un formato per la definizione di dati che è noto a un interprete di qualche natura.

Alcuni effetti collaterali interessanti si verificano quando si toglie l'operatore di assegnazione da una lingua, e questa è la vera causa della perdita di tutte quelle istruzioni imperative step1-step2-step3 in linguaggi dichiarativi.

Cosa fai con l'operatore di assegnazione in una funzione? Si creano passaggi intermedi. Questo è il punto cruciale, l'operatore di assegnazione viene utilizzato per modificare i dati in modo graduale. Non appena non è più possibile modificare i dati, si perdono tutti questi passaggi e si finisce con:

Ogni funzione ha una sola istruzione, questa è la singola dichiarazione

Ora ci sono molti modi per fare in modo che una singola istruzione assomigli a molte affermazioni, ma è solo un trucco, ad esempio: 1 + 3 + (2*4) + 8 - (7 / (8*3))questa è chiaramente una singola istruzione, ma se la scrivi come ...

1 +
3 +
(
  2*4
) +
8 - 
(
  7 / (8*3)
)

Può assumere l'aspetto di una sequenza di operazioni che può essere più facile per il cervello riconoscere la decomposizione desiderata che l'autore intendeva. Lo faccio spesso in C # con codice come ..

people
  .Where(person => person.MiddleName != null)
  .Select(person => person.MiddleName)
  .Distinct();

Questa è una singola affermazione, ma mostra l'aspetto di essere scomposto in molti - nota che non ci sono compiti.

Inoltre, in entrambi i metodi precedenti, il modo in cui il codice viene effettivamente eseguito non è nell'ordine in cui lo hai letto immediatamente; perché questo codice dice cosa fare, ma non stabilisce come . Nota che la semplice aritmetica sopra non sarà ovviamente eseguita nella sequenza scritta da sinistra a destra, e nell'esempio C # sopra quei 3 metodi sono effettivamente tutti rientranti e non eseguiti fino al completamento in sequenza, il compilatore in effetti genera codice che farà ciò che vuole quell'affermazione , ma non necessariamente come si assume.

Credo che abituarsi a questo approccio senza passaggi intermedi della codifica dichiarativa sia davvero la parte più difficile di tutto; e perché Haskell è così complicato perché poche lingue lo vietano davvero come fa Haskell. Devi iniziare a fare qualche ginnastica interessante per realizzare alcune cose che normalmente faresti con l'aiuto di variabili intermedie, come la somma.

In un linguaggio dichiarativo, se vuoi che una variabile intermedia faccia qualcosa - significa passarla come parametro a una funzione che fa quella cosa. Ecco perché la ricorsione diventa così importante.

sum xs = (head xs) + sum (tail xs)

Non puoi creare una resultSumvariabile e aggiungerla ad essa mentre esegui il ciclo continuo xs, devi semplicemente prendere il primo valore e aggiungerlo a qualunque sia la somma di tutto il resto - e per accedere a tutto il resto devi passare xsa una funzione tailperché non puoi semplicemente creare una variabile per xs e togliere la testa che sarebbe un approccio imperativo. (Sì, lo so che potresti usare la destrutturazione, ma questo esempio vuole essere illustrativo)


Il modo in cui lo capisco, dalla tua risposta, è che nella pura programmazione funzionale, l'intero programma è costituito da molte funzioni a espressione singola, mentre nelle funzioni di programmazione imperativa (procedure) contengono più di un'espressione con una sequenza operativa definita
ALXGTV

1 + 3 + (2 * 4) + 8 - (7 / (8 * 3)) sono in realtà più istruzioni / espressioni, ovviamente dipende da ciò che vedi come una singola espressione. Per riassumere, la programmazione funzionale è sostanzialmente una programmazione senza punto e virgola.
ALXGTV,

1
@ALXGTV la differenza tra quella dichiarazione e uno stile imperativo è che se quell'espressione matematica fosse affermazioni, sarebbe imperativo che vengano eseguite come scritte. Tuttavia, poiché non è una sequenza di affermazioni imperative, ma piuttosto un'espressione che definisce ciò che vuoi, perché non dice come , non è indispensabile che venga eseguito come scritto. Pertanto la compilazione può ridurre le espressioni o persino memorizzarle come letterali. Anche le lingue imperative lo fanno, ma solo quando lo metti in una singola affermazione; una dichiarazione.
Jimmy Hoffa,

Le dichiarazioni di @ALXGTV definiscono le relazioni, le relazioni sono malleabili; se x = 1 + 2allora x = 3e x = 4 - 1. Le istruzioni in sequenza definiscono le istruzioni in modo tale che quando x = (y = 1; z = 2 + y; return z;)non hai più qualcosa di malleabile, qualcosa che il compilatore può fare in diversi modi - piuttosto è indispensabile che il compilatore faccia quello che hai indicato lì, perché il compilatore non può conoscere tutti gli effetti collaterali delle tue istruzioni, quindi può ' non modificarli.
Jimmy Hoffa,

Hai un punto giusto.
ALXGTV,

6

So di essere in ritardo alla festa, ma l'altro giorno ho avuto un'epifania, quindi eccola ...

Penso che i commenti su funzioni funzionali, immutabili e senza effetti collaterali manchino il segno quando si spiega la differenza tra dichiarativo e imperativo o si spiega che cosa significhi programmazione dichiarativa. Inoltre, come accennato nella tua domanda, l'intero "cosa fare" vs "come farlo" è troppo vago e in realtà non spiega neanche.

Prendiamo il codice semplice a = b + ccome base e visualizziamo la dichiarazione in alcune lingue diverse per avere l'idea:

Quando scriviamo a = b + cin un linguaggio imperativo, come C, stiamo assegnando il valore correnteb + c alla variabile ae nient'altro. Non stiamo facendo alcuna dichiarazione fondamentale su ciò che aè. Piuttosto, stiamo semplicemente eseguendo un passaggio in un processo.

Quando scriviamo a = b + cin un linguaggio dichiarativo, come Microsoft Excel, (sì, Excel è un linguaggio di programmazione e probabilmente il più dichiarativo di tutti,) stiamo affermando una relazione tra a, be ctale che è sempre il caso che aè la somma di Gli altri due. Non è un passo in un processo, è un invariante, una garanzia, una dichiarazione di verità.

Anche i linguaggi funzionali sono dichiarativi, ma quasi per caso. In Haskel ad esempio a = b + csi afferma anche una relazione invariante, ma solo perché be csono immutabili.

Quindi sì, quando gli oggetti sono immutabili e le funzioni sono prive di effetti collaterali, il codice diventa dichiarativo (anche se sembra identico al codice imperativo), ma non è questo il punto. Né è evitare l'incarico. Il punto del codice dichiarativo è fare dichiarazioni fondamentali sulle relazioni.


0

Hai ragione sul fatto che non esiste una chiara distinzione tra dire al computer cosa fare e come farlo.

Tuttavia, da un lato dello spettro si pensa quasi esclusivamente a come manipolare la memoria. Cioè, per risolvere un problema, lo si presenta al computer in una forma come "imposta questa posizione di memoria su x, quindi imposta quella posizione di memoria su y, passa alla posizione di memoria z ..." e poi in qualche modo finisci per avere il risultato in un'altra posizione di memoria.

In linguaggi gestiti come Java, C # e così via, non si ha più accesso diretto alla memoria hardware. Il programmatore imperativo ora si occupa di variazioni statiche, riferimenti o campi di istanze di classe, che sono tutte in qualche misura astrazioni per posizioni di memoria.

In lingue come Haskell, OTOH, la memoria è completamente sparita. Semplicemente non è così

f a b = let y = sum [a..b] in y*y

ci devono essere due celle di memoria che contengono gli arguenti a e b e un'altra che contiene il risultato intermedio y. A dire il vero, un back-end del compilatore potrebbe emettere il codice finale che funziona in questo modo (e in un certo senso deve farlo fino a quando l'architettura di destinazione è una macchina v. Neumann).

Ma il punto è che non abbiamo bisogno di interiorizzare l'architettura v. Neumann per capire la funzione sopra. Né abbiamo bisogno di un computer contemporaneo per eseguirlo. Sarebbe facile tradurre i programmi in un linguaggio FP puro in una macchina ipotetica che funziona sulla base del calcolo SKI, per esempio. Ora prova lo stesso con un programma C!

Un linguaggio dichiarativo puro per me sarebbe uno che è composto esclusivamente da dichiarazioni

Questo non è abbastanza forte, IMHO. Anche un programma C è solo una sequenza di dichiarazioni. Sento che dobbiamo qualificare ulteriormente le dichiarazioni. Ad esempio, ci stanno dicendo cosa è (dichiarativo) o cosa fa (imperativo).


Non ho affermato che la programmazione funzionale non fosse diversa dalla programmazione in C, in effetti ho detto che linguaggi come Haskell sono a un livello MOLTO più alto di C e offrono un'astrazione molto maggiore.
ALXGTV,
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.