Comprendere veramente la differenza tra procedurale e funzionale


114

Sto davvero facendo fatica a capire la differenza tra paradigmi di programmazione procedurale e funzionale .

Ecco i primi due paragrafi dalla voce di Wikipedia sulla programmazione funzionale :

In informatica, la programmazione funzionale è un paradigma di programmazione che tratta il calcolo come la valutazione di funzioni matematiche ed evita dati di stato e mutabili. Enfatizza l'applicazione delle funzioni, in contrasto con lo stile di programmazione imperativo, che enfatizza i cambiamenti di stato. La programmazione funzionale ha le sue radici nel lambda calcolo, un sistema formale sviluppato negli anni '30 per indagare la definizione di funzione, l'applicazione di funzioni e la ricorsione. Molti linguaggi di programmazione funzionale possono essere visti come elaborazioni sul lambda calcolo.

In pratica, la differenza tra una funzione matematica e la nozione di "funzione" utilizzata nella programmazione imperativa è che le funzioni imperative possono avere effetti collaterali, modificando il valore dello stato del programma. Per questo motivo mancano di trasparenza referenziale, ovvero la stessa espressione di linguaggio può produrre valori diversi in momenti diversi a seconda dello stato del programma in esecuzione. Al contrario, nel codice funzionale, il valore di output di una funzione dipende solo dagli argomenti che vengono inseriti nella funzione, quindi chiamare una funzione fdue volte con lo stesso valore per un argomento xprodurrà lo stesso risultatof(x)entrambe le volte. L'eliminazione degli effetti collaterali può rendere molto più facile comprendere e prevedere il comportamento di un programma, che è una delle motivazioni chiave per lo sviluppo della programmazione funzionale.

Al paragrafo 2 dove si dice

Al contrario, nel codice funzionale, il valore di output di una funzione dipende solo dagli argomenti inseriti nella funzione, quindi chiamare una funzione fdue volte con lo stesso valore per un argomento xprodurrà lo stesso risultato f(x)entrambe le volte.

Non è lo stesso caso esatto per la programmazione procedurale?

Cosa si dovrebbe cercare in procedurali vs funzionali che si distinguono?


1
Il collegamento "Charming Python: Functional Programming in Python" di Abafei è stato interrotto. Ecco una buona serie di collegamenti: ibm.com/developerworks/linux/library/l-prog/index.html ibm.com/developerworks/linux/library/l-prog2/index.html
Chris Koknat

Un altro aspetto di questo è la denominazione. Per esempio. in JavaScript e Common Lisp usiamo il termine funzione anche se sono consentiti effetti collaterali e in Scheme it lo stesso viene costantemente chiamato proceduere. Una funzione CL pura può essere scritta come una procedura Scheme funzionale pura. Quasi tutti i libri su Scheme utilizzano il termine procedura poiché è il tyerm utilizzato nello standard e non ha nulla a che fare con il fatto che sia procedurale o funzionale.
Sylwester

Risposte:


276

Programmazione funzionale

La programmazione funzionale si riferisce alla capacità di trattare le funzioni come valori.

Consideriamo un'analogia con i valori "regolari". Possiamo prendere due valori interi e combinarli usando l' +operatore per ottenere un nuovo numero intero. Oppure possiamo moltiplicare un numero intero per un numero in virgola mobile per ottenere un numero in virgola mobile.

Nella programmazione funzionale, possiamo combinare due valori di funzione per produrre un nuovo valore di funzione utilizzando operatori come compose o lift . Oppure possiamo combinare un valore di funzione e un valore di dati per produrre un nuovo valore di dati utilizzando operatori come map o fold .

Si noti che molti linguaggi hanno capacità di programmazione funzionale, anche linguaggi che di solito non sono considerati linguaggi funzionali. Anche il nonno FORTRAN supportava i valori delle funzioni, sebbene non offrisse molto in termini di combinazione di operatori. Affinché un linguaggio sia definito "funzionale", deve abbracciare le capacità di programmazione funzionale in grande stile.

Programmazione procedurale

La programmazione procedurale si riferisce alla capacità di incapsulare una sequenza comune di istruzioni in una procedura in modo che tali istruzioni possano essere richiamate da molti punti senza ricorrere al copia e incolla. Poiché le procedure erano uno sviluppo molto precoce della programmazione, la capacità è quasi invariabilmente collegata allo stile di programmazione richiesto dalla programmazione in linguaggio macchina o assembly: uno stile che enfatizza la nozione di posizioni di archiviazione e istruzioni che spostano i dati tra quelle posizioni.

Contrasto

I due stili non sono realmente opposti: sono solo diversi l'uno dall'altro. Ci sono linguaggi che abbracciano pienamente entrambi gli stili (LISP, per esempio). Il seguente scenario può dare un'idea di alcune differenze nei due stili. Scriviamo del codice per un requisito senza senso in cui vogliamo determinare se tutte le parole in un elenco hanno un numero dispari di caratteri. Primo, stile procedurale:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

Prendo per scontato che questo esempio sia comprensibile. Ora, stile funzionale:

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

Lavorando dall'interno verso l'esterno, questa definizione fa le seguenti cose:

  1. compose(odd, length)combina le funzioni odde lengthper produrre una nuova funzione che determina se la lunghezza di una stringa è dispari.
  2. map(..., words)chiama quella nuova funzione per ogni elemento in words, restituendo infine un nuovo elenco di valori booleani, ognuno dei quali indica se la parola corrispondente ha un numero dispari di caratteri.
  3. apply(and, ...)applica l'operatore "and" alla lista risultante, e -unendo tutti i valori booleani insieme per ottenere il risultato finale.

Puoi vedere da questi esempi che la programmazione procedurale è molto interessata allo spostamento dei valori nelle variabili e alla descrizione esplicita delle operazioni necessarie per produrre il risultato finale. Al contrario, lo stile funzionale enfatizza la combinazione di funzioni necessarie per trasformare l'input iniziale nell'output finale.

L'esempio mostra anche le dimensioni relative tipiche del codice procedurale rispetto a quello funzionale. Inoltre, dimostra che le caratteristiche delle prestazioni del codice procedurale potrebbero essere più facili da vedere rispetto a quelle del codice funzionale. Considera: le funzioni calcolano le lunghezze di tutte le parole nell'elenco o ciascuna si interrompe immediatamente dopo aver trovato la prima parola di lunghezza pari? D'altra parte, il codice funzionale consente un'implementazione di alta qualità per eseguire un'ottimizzazione piuttosto seria poiché esprime principalmente l'intento piuttosto che un algoritmo esplicito.

Ulteriori letture

Questa domanda sorge spesso ... vedi, ad esempio:

La conferenza del premio Turing di John Backus espone in grande dettaglio le motivazioni per la programmazione funzionale:

Può la programmazione essere liberata dallo stile von Neumann?

Non dovrei davvero menzionare quel documento nel contesto attuale perché diventa piuttosto tecnico, abbastanza rapidamente. Non ho potuto resistere perché penso che sia davvero fondamentale.


Addendum - 2013

I commentatori sottolineano che i linguaggi contemporanei popolari offrono altri stili di programmazione oltre procedurale e funzionale. Tali linguaggi offrono spesso uno o più dei seguenti stili di programmazione:

  • query (ad es. liste di comprensione, query integrata nel linguaggio)
  • flusso di dati (ad es. iterazione implicita, operazioni in blocco)
  • orientato agli oggetti (ad es. dati e metodi incapsulati)
  • orientato al linguaggio (ad es. sintassi specifica dell'applicazione, macro)

Vedere i commenti seguenti per esempi di come gli esempi di pseudo-codice in questa risposta possono trarre vantaggio da alcune delle funzionalità disponibili da quegli altri stili. In particolare, l'esempio procedurale trarrà vantaggio dall'applicazione di praticamente qualsiasi costrutto di livello superiore.

Gli esempi esposti evitano deliberatamente di mescolarsi a questi altri stili di programmazione per enfatizzare la distinzione tra i due stili in discussione.


1
Risposta davvero carina, ma potresti semplificare un po 'il codice, ad esempio: "function allOdd (words) {foreach (auto word in words) {odd (length (word)? Return false:;} return true;}"
Dainius

Lo stile funzionale è piuttosto difficile da leggere rispetto allo "stile funzionale" in python: def odd_words (words): return [x for x in words if dispd (len (x))]
boxed

@boxed: la tua odd_words(words)definizione fa qualcosa di diverso da quello della risposta allOdd. Per il filtraggio e la mappatura, spesso si preferiscono le liste di comprensione, ma qui la funzione allOdddovrebbe ridurre un elenco di parole a un singolo valore booleano.
ShinNoNoir

@WReach: avrei scritto il tuo esempio funzionale in questo modo: function allOdd (words) {return and (odd (length (first (words))), allOdd (rest (words))); } Non è più elegante del tuo esempio, ma in un linguaggio ricorsivo di coda avrebbe le stesse caratteristiche di prestazione dello stile imperativo.
mishoo

@mishoo Il linguaggio deve essere sia ricorsivo di coda che rigoroso e cortocircuitante nell'operatore e affinché la tua ipotesi sia valida, credo.
kqr

46

La vera differenza tra programmazione funzionale e imperativa è la mentalità: i programmatori imperativi pensano a variabili e blocchi di memoria, mentre i programmatori funzionali pensano: "Come posso trasformare i miei dati di input nei miei dati di output" - il tuo "programma" è la pipeline e un insieme di trasformazioni sui dati per portarli dall'Input all'Output. Questa è la parte interessante IMO, non il bit "Non dovrai usare variabili".

Come conseguenza di questa mentalità, i programmi FP descrivono in genere ciò che accadrà, invece del meccanismo specifico di come accadrà: questo è potente perché se possiamo affermare chiaramente cosa significano "Seleziona" e "Dove" e "Aggrega", noi sono liberi di sostituire le loro implementazioni, proprio come facciamo con AsParallel () e improvvisamente la nostra app a thread singolo si ridimensiona a n core.


in qualche modo puoi contrastare i due usando frammenti di esempio di codice? lo apprezzo davvero
Filossofo

1
@KerxPhilo: ecco un compito molto semplice (aggiungi numeri da 1 a n). Imperativo: modificare il numero corrente, modificare la somma finora. Codice: int i, sum; somma = 0; per (i = 1; i <= n; i ++) {somma + = i; }. Funzionale (Haskell): prendi un elenco pigro di numeri, piegali insieme aggiungendo a zero. Codice: foldl (+) 0 [1..n]. Spiacenti, nessuna formattazione nei commenti.
Dirkt

+1 alla risposta. In altre parole, la programmazione funzionale riguarda la scrittura di funzioni senza effetti collaterali quando possibile, ovvero la funzione restituisce sempre la stessa cosa quando vengono forniti gli stessi parametri: questo è il fondamento. Se segui questo approccio all'estremo, i tuoi effetti collaterali (ne hai sempre bisogno) saranno isolati e il resto delle funzioni trasformerà semplicemente i dati di input in dati di output.
beluchin

12
     Isn't that the same exact case for procedural programming?

No, perché il codice procedurale può avere effetti collaterali. Ad esempio, può memorizzare lo stato tra le chiamate.

Detto questo, è possibile scrivere codice che soddisfi questo vincolo in linguaggi considerati procedurali. Ed è anche possibile scrivere codice che rompa questo vincolo in alcuni linguaggi considerati funzionali.


1
Puoi mostrare un esempio e un confronto? Apprezzo davvero se potessi.
Philoxopher

8
La funzione rand () in C fornisce un risultato diverso per ogni chiamata. Memorizza lo stato tra le chiamate. Non è referenzialmente trasparente. In confronto, std :: max (a, b) in C ++ restituirà sempre lo stesso risultato con gli stessi argomenti e non ha effetti collaterali (che io sappia ...).
Andy Thomas

11

Non sono d'accordo con la risposta di WReach. Decostruiamo un po 'la sua risposta per vedere da dove viene il disaccordo.

Innanzitutto, il suo codice:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

e

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

La prima cosa da notare è che sta fondendo:

  • Funzionale
  • Orientato all'espressione e
  • Iteratore centrico

programmazione e manca la possibilità per la programmazione in stile iterativo di avere un flusso di controllo più esplicito rispetto a un tipico stile funzionale.

Parliamo rapidamente di questi.

Lo stile incentrato sull'espressione è quello in cui le cose, per quanto possibile, valutano le cose. Sebbene i linguaggi funzionali siano famosi per il loro amore per le espressioni, in realtà è possibile avere un linguaggio funzionale senza espressioni componibili. Ho intenzione di fare uno, dove non ci sono nessun espressioni, soltanto dichiarazioni.

lengths: map words length
each_odd: map lengths odd
all_odd: reduce each_odd and

Questo è più o meno lo stesso di quanto detto prima, tranne per il fatto che le funzioni sono concatenate puramente attraverso catene di istruzioni e associazioni.

Uno stile di programmazione incentrato sull'iteratore potrebbe essere quello adottato da Python. Usiamo uno stile puramente iterativo, incentrato sull'iteratore:

def all_odd(words):
    lengths = (len(word) for word in words)
    each_odd = (odd(length) for length in lengths)
    return all(each_odd)

Questo non è funzionale, perché ogni clausola è un processo iterativo e sono legate insieme da una pausa esplicita e dalla ripresa degli stack frame. La sintassi può essere parzialmente ispirata da un linguaggio funzionale, ma viene applicata a una sua incarnazione completamente iterativa.

Certo, puoi comprimere questo:

def all_odd(words):
    return all(odd(len(word)) for word in words)

L'imperativo non sembra poi così male ora, eh? :)

Il punto finale riguardava un flusso di controllo più esplicito. Riscriviamo il codice originale per utilizzarlo:

function allOdd(words) {
    for (var i = 0; i < length(words); ++i) {
        if (!odd(length(words[i]))) {
            return false;
        }
    }
    return true;
}

Usando gli iteratori puoi avere:

function allOdd(words) {
    for (word : words) { if (!odd(length(word))) { return false; } }
    return true;
}

Allora qual è il punto di un linguaggio funzionale se la differenza è tra:

return all(odd(len(word)) for word in words)
return apply(and, map(compose(odd, length), words))
for (word : words) { if (!odd(length(word))) { return false; } }
return true;


La principale caratteristica definitiva di un linguaggio di programmazione funzionale è che rimuove la mutazione come parte del tipico modello di programmazione. Le persone spesso pensano che questo significhi che un linguaggio di programmazione funzionale non ha dichiarazioni o usa espressioni, ma queste sono semplificazioni. Un linguaggio funzionale sostituisce il calcolo esplicito con una dichiarazione di comportamento, sulla quale il linguaggio esegue quindi una riduzione.

Limitare te stesso a questo sottoinsieme di funzionalità ti permette di avere maggiori garanzie sui comportamenti dei tuoi programmi, e questo ti permette di comporli più liberamente.

Quando si dispone di un linguaggio funzionale, creare nuove funzioni è generalmente semplice come comporre funzioni strettamente correlate.

all = partial(apply, and)

Questo non è semplice, o forse nemmeno possibile, se non hai controllato esplicitamente le dipendenze globali di una funzione. La caratteristica migliore della programmazione funzionale è che puoi creare costantemente astrazioni più generiche e avere fiducia che possano essere combinate in un insieme più grande.


Sai, sono abbastanza sicuro che un applynon è esattamente la stessa operazione di un foldo reduce, anche se sono d'accordo con la bella capacità di avere algoritmi molto generici.
Benedict Lee

Non ho mai sentito parlare di applysignificare foldo reduce, ma sembra a me come deve essere in questo contesto, per poter restituire un valore booleano.
Veedrac

Ah, ok, sono stato confuso dalla denominazione. Grazie per aver chiarito tutto.
Benedict Lee

6

Nel paradigma procedurale (dovrei dire invece "programmazione strutturata"?), Hai condiviso memoria mutevole e istruzioni che la leggono / scrivono in una sequenza (una dopo l'altra).

Nel paradigma funzionale, hai variabili e funzioni (in senso matematico: le variabili non variano nel tempo, le funzioni possono solo calcolare qualcosa in base ai loro input).

(Questo è estremamente semplificato, ad esempio, gli FPL in genere hanno strutture per lavorare con la memoria mutabile mentre i linguaggi procedurali possono spesso supportare procedure di ordine superiore quindi le cose non sono così chiare; ma questo dovrebbe darti un'idea)



2

Nella programmazione funzionale per ragionare sul significato di un simbolo (nome di una variabile o di una funzione) è necessario conoscere solo 2 cose: l'ambito corrente e il nome del simbolo. Se si dispone di un linguaggio puramente funzionale con immutabilità, entrambi sono concetti "statici" (scusate per il nome sovraccarico), il che significa che potete vedere entrambi - l'ambito corrente e il nome - semplicemente guardando il codice sorgente.

Nella programmazione procedurale se vuoi rispondere alla domanda qual è il valore dietro xdevi anche sapere come ci sei arrivato, l'ambito e il nome da soli non sono sufficienti. E questa è quella che vedrei come la sfida più grande perché questo percorso di esecuzione è una proprietà "runtime" e può dipendere da così tante cose diverse, che la maggior parte delle persone impara a eseguirne il debug e non a cercare di recuperare il percorso di esecuzione.



0

Una chiara differenza tra i due paradigmi di programmazione è lo stato.

Nella programmazione funzionale, lo stato viene evitato. In poche parole, non ci sarà alcuna variabile a cui viene assegnato un valore.

Esempio:

def double(x):
    return x * 2

def doubleLst(lst):
    return list(map(double, action))

Tuttavia, la programmazione procedurale utilizza lo stato.

Esempio:

def doubleLst(lst):
    for i in range(len(lst)):
        lst[i] = lst[i] * 2  # assigning of value i.e. mutation of state
    return lst
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.