Esiste davvero l'immutabilità nella programmazione funzionale?


9

Anche se lavoro come programmatore nella mia vita quotidiana e utilizzo tutti i linguaggi di tendenza (Python, Java, C, ecc.), Non ho ancora una visione chiara di cosa sia la programmazione funzionale. Da quello che ho letto, una proprietà dei linguaggi funzionali è che le strutture di dati sono immutabili . Per me questo da solo solleva molte domande. Ma prima scriverò un po 'di ciò che capisco dell'immutabilità e, se sbaglio, mi sento libero di correggermi.

La mia comprensione dell'immutabilità:

  • All'avvio di un programma, ha strutture dati fisse con dati fissi
  • Non è possibile aggiungere nuovi dati a queste strutture
  • Non ci sono variabili nel codice
  • Puoi semplicemente "copiare" dai dati già o dai dati attualmente calcolati
  • A causa di quanto sopra, l'immutabilità aggiunge un'enorme complessità spaziale a un programma

Le mie domande:

  1. Se le strutture di dati dovrebbero rimanere come sono (immutabili), come diavolo qualcuno aggiunge un nuovo elemento in un elenco?
  2. Qual è il punto di avere un programma che non può ottenere nuovi dati? Supponi di avere un sensore collegato al tuo computer che desidera inviare dati al programma. Ciò significherebbe che non possiamo archiviare i dati in arrivo da nessuna parte?
  3. In che modo la programmazione funzionale è utile per l'apprendimento automatico? Poiché l'apprendimento automatico si basa sul presupposto di aggiornare la "percezione" del programma delle cose, memorizzando così nuovi dati.

2
Non sono d'accordo con te quando dici che non ci sono variabili nel codice funzionale. Esistono variabili nel senso matematico di "Una quantità che può assumere uno qualsiasi di un insieme di valori". Non sono mutevoli , certo, ma nemmeno in matematica.
Édouard,

1
Penso che la tua confusione sia dovuta al fatto che stai pensando ai linguaggi funzionali in modo astratto. Basta prendere qualsiasi programma in Haskell - ad esempio un programma che legge un elenco di numeri dalla console, lo ordina rapidamente e lo emette - e capire come funziona e come confuta i tuoi sospetti. Non c'è modo di chiarire davvero le cose senza guardare esempi di programmi reali piuttosto che filosofare. Troverai molti programmi in qualsiasi tutorial di Haskell.
jkff,

@jkff Cosa stai cercando di dire? Che Haskel ha caratteristiche non funzionali. La domanda non riguarda Haskell, ma la programmazione funzionale. O stai affermando che tutto ciò che è funzionale? Come? Quindi cosa dovrebbe esserci di sbagliato nel filosofare, come dici tu. In che modo l'astrazione confonde? La domanda del PO è molto ragionevole.
babou,

@babou Sto cercando di dire che il modo migliore per capire come un linguaggio di programmazione puramente funzionale può implementare in modo efficiente algoritmi e strutture di dati è quello di guardare esempi di algoritmi e strutture di dati implementati in modo efficiente in un linguaggio di programmazione funzionale. Mi sembra che OP stesse cercando di capire come sia concettualmente possibile - penso che il modo più veloce per capirlo sia guardare esempi, piuttosto che leggere una spiegazione concettuale, non importa quanto sia dettagliato.
jkff,

Un modo di guardare alla programmazione funzionale è quello di dire che sta programmando senza effetti collaterali. Puoi farlo nella tua lingua "alla moda" di scelta. Non evitare tutte le riassegnazioni: ad es. In Java, tutte le variabili saranno definitive e tutti i metodi saranno di sola lettura.
reinierpost,

Risposte:


10

All'avvio di un programma, ha strutture dati fisse con dati fissi

Questo è un po 'un malinteso. Ha una forma fissa e una serie fissa di regole di riscrittura, ma queste regole di riscrittura possono esplodere in qualcosa di molto più grande. Ad esempio, l'espressione [1..100000000] in Haskell è rappresentata da una quantità molto piccola di codice ma la sua forma normale è enorme.

Non è possibile aggiungere nuovi dati a queste strutture

Sì e no. Il sottoinsieme puramente funzionale di un linguaggio come Haskell o ML non può ottenere dati dal mondo esterno ma qualsiasi linguaggio per la programmazione pratica ha un meccanismo per inserire dati dal mondo esterno nel sottoinsieme puramente funzionale. In Haskell questo viene fatto con molta attenzione, ma in ML puoi farlo quando vuoi.

Non ci sono variabili nel codice

Questo è praticamente vero, ma non confonderlo con l'idea che non si può nominare nulla. Dai sempre un nome a espressioni utili e le riutilizzi costantemente. Anche ML e Haskell, ogni Lisp che ho provato, e ibridi come Scala, hanno tutti un mezzo per creare variabili. Semplicemente non sono comunemente usati. E ancora i sottoinsiemi puramente funzionali di tali lingue non li hanno.

Puoi semplicemente "copiare" dai dati già o dai dati attualmente calcolati

È possibile eseguire il calcolo mediante riduzione alla forma normale. La cosa migliore da fare è probabilmente andare a scrivere programmi in un linguaggio funzionale per vedere come effettivamente eseguono i calcoli.

Ad esempio, "sum [1..1000]" non è un calcolo che voglio eseguire ma è fatto abbastanza facilmente da Haskell. Gli abbiamo dato una piccola espressione che aveva significato per noi e Haskell ci ha dato il numero corrispondente. Quindi esegue sicuramente il calcolo.

Se le strutture di dati dovrebbero rimanere come sono (immutabili), come diavolo qualcuno aggiunge un nuovo elemento in un elenco?

Non aggiungi un nuovo elemento a un elenco, crei un nuovo elenco da quello vecchio. Poiché il vecchio non può essere mutato, è perfettamente sicuro utilizzarlo nella nuova lista o ovunque tu voglia. Molti più dati possono essere condivisi in modo sicuro in questo schema.

Qual è il punto di avere un programma che non può ottenere nuovi dati? Supponi di avere un sensore collegato al tuo computer che desidera inviare dati al programma. Ciò significherebbe che non possiamo archiviare i dati in arrivo da nessuna parte?

Per quanto riguarda l'input dell'utente, qualsiasi linguaggio di programmazione pratico ha un modo per ottenere l'input dell'utente. Questo succede. Tuttavia, esiste un sottoinsieme completamente funzionale di queste lingue in cui si scrive la maggior parte del codice e si raccolgono i vantaggi in questo modo.

In che modo la programmazione funzionale è utile per l'apprendimento automatico? Poiché l'apprendimento automatico si basa sul presupposto di aggiornare la "percezione" del programma delle cose, memorizzando così nuovi dati.

Questo sarebbe il caso dell'apprendimento attivo, ma la maggior parte dell'apprendimento automatico con cui ho lavorato (lavoro come scimmia codice in un gruppo di apprendimento automatico e lo faccio da alcuni anni) prevede un processo di apprendimento unico in cui vengono caricati tutti i dati di addestramento in una volta. Ma per l'apprendimento attivo non puoi fare le cose al 100% in modo puramente funzionale. Dovrai leggere alcuni dati dal mondo esterno.


Sento che hai convenientemente ignorato quale potrebbe essere probabilmente il punto più importante nel post di @ Pithikos, che è il problema dello spazio - i programmi funzionali usano più spazio di quelli imperativi (non puoi scrivere algoritmi sul posto e simili)
user541686

2
Questo semplicemente non è vero. La mancanza di mutazione è in gran parte compensata dalla condivisione e in cima a tutto ciò la differenza di dimensioni a cui ti riferisci è decisamente piccola con i compilatori moderni. La maggior parte del codice sugli elenchi di haskell è effettivamente in atto o non utilizza alcuna memoria.
Jake,

1
Penso che tu abbia travisato in qualche modo ML. Sì, l'I / O può avvenire ovunque, ma il modo in cui le nuove informazioni vengono introdotte nelle strutture esistenti è strettamente controllato.
Dfeuer,

@Pithikos, ci sono variabili dappertutto; sono solo diversi da quelli a cui sei abituato, come indicato da Édouard. E le cose vengono continuamente allocate e la spazzatura raccolta. Una volta che sei effettivamente entrato nella programmazione funzionale, avrai una migliore idea di come va effettivamente.
Dfeuer,

1
È vero che esistono algoritmi che non hanno un'implementazione puramente funzionale con la stessa complessità temporale dell'implementazione imperativa più nota - ad esempio la struttura di dati Union-Find (e, um, array :)) Immagino che ci siano anche casi come questo per lo spazio complessità. Ma queste sono eccezioni: algoritmi / strutture dati più comuni hanno implementazioni con complessità di tempo e spazio equivalenti. È una questione soggettiva di stile di programmazione e (a un fattore costante) di qualità del compilatore.
jkff,

4

Immutabilità o mutabilità non sono concetti che hanno senso nella programmazione funzionale.

Il contesto computazionale

Questa è un'ottima domanda che è un seguito interessante (non un duplicato) a un altro recente: qual è la differenza tra assegnazione, valutazione e associazione dei nomi?

Piuttosto, rispondendo alle tue dichiarazioni una per una, sto provando qui a darti una visione strutturata di ciò che è in gioco.

Esistono diversi problemi da considerare per rispondere, tra cui:

  • Che cos'è un modello di calcolo e quali concetti hanno senso per un determinato modello

  • Qual è il significato delle parole che stai usando e come dipende dal contesto

Lo stile di programmazione funzionale sembra sciocco perché lo vedi con un occhio imperativo da programmatore. Ma è un paradigma diverso e i tuoi concetti e percezione imperativi sono alieni, fuori posto. I compilatori non hanno tali pregiudizi.

Ma la conclusione finale è che è possibile scrivere programmi in modo puramente funzionale, anche per l'apprendimento automatico, ritenendo che la programmazione funzionale non abbia il concetto di memorizzare dati. Mi sembra di non essere d'accordo su questo punto con altre risposte.

Nella speranza alcuni saranno interessati nonostante la lunghezza di questa risposta.

Paradigmi computazionali

La domanda riguarda la programmazione funzionale (aka programmazione applicativa), un modello specifico di calcolo, il cui rappresentante teorico e più semplice è il calcolo lambda.

Se rimani a un livello teorico, ci sono molti modelli di calcolo: la macchina di Turing (TM), la macchina RAM e altri , il calcolo lambda, la logica combinatoria, la teoria delle funzioni ricorsive, i sistemi semi-Thue, ecc. Il più potente computazionale i modelli sono stati dimostrati equivalenti in termini di ciò a cui possono rivolgersi, e questo è l'essenza della tesi di Church-Turing .

Un concetto importante è la riduzione reciproca dei modelli, che è la base per stabilire le equivalenze che portano alla tesi Church-Turing. Visto dal punto di vista dei programmatori, ridurre un modello a un altro è praticamente quello che viene solitamente chiamato un compilatore. Se prendi la programmazione logica come modello di calcolo, è molto diversa dal modello fornito dal PC acquistato in un negozio e il compilatore traduce i programmi scritti nel linguaggio di programmazione logica nel modello computazionale rappresentato dal tuo PC (praticamente il computer RAM).

β

In pratica, i linguaggi di programmazione che utilizziamo tendono a mescolare concetti di diverse origini teoriche, provando a farlo in modo che parti selezionate di un programma possano beneficiare delle proprietà di alcuni modelli ove appropriato. Allo stesso modo, le persone che costruiscono sistemi possono scegliere lingue diverse per componenti diversi, per adattare al meglio la lingua all'attività da svolgere.

Quindi, raramente vedi un paradigma di programmazione allo stato puro in un linguaggio di programmazione. I linguaggi di programmazione sono ancora classificati in base al paradigma dominante, ma le proprietà del linguaggio possono essere influenzate quando sono coinvolti concetti di altri paradigmi, spesso confondendo distinzioni e problemi concettuali.

Tipicamente, linguaggi come Haskell e ML o CAML sono considerati funzionali, ma possono consentire comportamenti imperativi ... Altrimenti perché si dovrebbe parlare del " sottoinsieme puramente funzionale "?

Quindi si può affermare che si può fare questo o quello nel mio linguaggio di programmazione funzionale, ma in realtà non risponde a una domanda sulla programmazione funzionale quando si basa su ciò che può essere considerato extra-funzionale.

Le risposte dovrebbero essere più precisamente correlate a un paradigma specifico, senza gli extra.

Cos'è una variabile?

Un altro problema è l'uso della terminologia. In matematica una variabile è un'entità che rappresenta un valore indeterminato in alcuni domini. È usato per vari scopi. Utilizzato in un'equazione, può rappresentare qualsiasi valore tale da verificare l'equazione. Questa visione è utilizzata nella programmazione logica sotto il nome di " variabile logica ", probabilmente perché la variabile nome aveva già un altro significato quando è stata sviluppata la programmazione logica.

Nella programmazione imperativa tradizionale, una variabile è intesa come una sorta di contenitore (o posizione di memoria) che può memorizzare la rappresentazione di un valore e possibilmente far sostituire il suo valore corrente con un altro).

Nella programmazione funzionale, una variabile ha lo stesso scopo che ha in matematica di un segnaposto per un certo valore, ancora da fornire. Nella programmazione imperativa tradizionale questo ruolo è in realtà svolto da costante (da non confondere con i letterali che sono determinati valori espressi con una notazione specifica per quel dominio di valori, come 123, true, ["abdcz", 3.14]).

Le variabili di qualunque tipo, oltre che costanti, possono essere rappresentate da identificatori.

È possibile modificare il valore della variabile imperativa e questa è la base della mutabilità. La variabile funzionale non può.

I linguaggi di programmazione di solito consentono di costruire entità più grandi da quelle più piccole nella lingua.

I linguaggi imperativi consentono a tali costrutti di includere variabili e questo è ciò che ti dà dati mutabili.

Come leggere un programma

Un programma è fondamentalmente una descrizione astratta del tuo algoritmo è un linguaggio, che sia un progetto pragmatico o un linguaggio paradigmaticamente puro.

In linea di principio, puoi prendere ogni affermazione per ciò che dovrebbe significare in modo astratto. Quindi il compilatore lo tradurrà in una forma appropriata per l'esecuzione del computer, ma questo non è il tuo problema in prima approssimazione.

Certo, la realtà è un po 'più dura, ed è spesso bene avere un'idea di ciò che accade in modo da evitare strutture che il compilatore non saprà affrontare per un'esecuzione efficiente. Ma questa è già ottimizzazione ... per la quale i compilatori possono essere molto utili, spesso meglio dei programmatori.

Programmazione funzionale e mutabilità

La mutabilità si basa sull'esistenza di variabili imperative che possono contenere valori, che possono essere modificati in base al compito. Dal momento che questi non esistono nella programmazione funzionale, tutto può essere visto come immutabile.

La programmazione funzionale si occupa esclusivamente di valori.

Le tue prime quattro affermazioni sull'immutabilità sono per lo più corrette, ma descrivono con una visione imperativa qualcosa che non è imperativo. È un po 'come descrivere con i colori in un mondo in cui ognuno è cieco. Stai usando concetti estranei alla programmazione funzionale.

Hai solo valori puri e una matrice di numeri interi è un valore puro. Per ottenere un altro array che differisce solo per un elemento, è necessario utilizzare un valore di array diverso. La modifica di un elemento è solo un concetto che non esiste in questo contesto. Potresti avere una funzione che ha un array e alcuni indici come argomento e restituisce un risultato che è un array quasi identico che differisce solo dove indicato dagli indici. Ma è ancora un valore di array indipendente. Come vengono rappresentati questi valori non è un tuo problema. Forse "condividono" molto nella traduzione imperativa per il computer ... ma quello è il lavoro del compilatore ... e non vuoi nemmeno sapere per quale tipo di architettura della macchina sta compilando.

Tu non copi i valori (non ha senso, è un concetto alieno). Utilizzi solo i valori esistenti nei domini che hai definito nel tuo programma. O li descrivi (come letterali) o sono il risultato dell'applicazione di una funzione ad altri valori. Puoi dare loro un nome (definendo così una costante) per assicurarti che lo stesso valore sia usato in diversi punti del programma. Si noti che l'applicazione della funzione non deve essere percepita come un calcolo ma come il risultato dell'applicazione agli argomenti forniti. Scrivere 5+2o scrivere 7equivale allo stesso. Che è coerente con il paragrafo precedente.

Non ci sono variabili imperative. Nessun incarico è possibile. È possibile associare i nomi solo ai valori (per formare costanti), diversamente dalle lingue imperative in cui è possibile associare i nomi a variabili assegnabili.

Non è chiaro se ciò abbia un costo in complessità. Per prima cosa, ti riferisci alla complessità riguarda paradigmi imperativi. Non è definito come tale per la programmazione funzionale, a meno che non si scelga di leggere un programma funzionale come imperativo, che non è l'intento del progettista. In effetti, la visione funzionale ha lo scopo di non preoccuparti di tali problemi e di concentrarti su ciò che viene calcolato. È un po 'come le specifiche rispetto all'implementazione.

Il compilatore deve occuparsi dell'implementazione ed essere abbastanza intelligente da adattare al meglio ciò che deve essere fatto all'hardware che lo farà, qualunque esso sia.

Non sto dicendo che i programmatori non se ne preoccupino mai. Inoltre non sto dicendo che i linguaggi di programmazione e la tecnologia del compilatore siano maturi come potremmo desiderare che siano.

Rispondere alle domande

  1. Non si modifica il valore esistente (concetto alieno), ma si calcolano nuovi valori che differiscono dove desiderato, possibilmente avendo un elemento in più è un elenco.

  2. Il programma può ottenere nuovi dati. Il punto è come lo esprimi nella lingua. Ad esempio, puoi considerare che il programma funziona con un valore specifico, possibilmente di dimensioni illimitate, chiamato flusso di input. È un valore che dovrebbe essere seduto lì (se è già noto completamente o no non è un tuo problema). Quindi hai una funzione che restituisce una coppia composta dal primo elemento del flusso e dal resto del flusso.

    Puoi usarlo per costruire reti di componenti comunicanti in un modo puramente applicativo (coroutine)

  3. L'apprendimento automatico è solo un altro problema quando si devono cancellare dati e modificare i valori. Nella programmazione funzionale non lo fai: devi solo calcolare nuovi valori che differiscono in modo appropriato in base ai dati di allenamento. Anche la macchina risultante funzionerà. Ciò di cui ti preoccupi è il tempo di elaborazione e l'efficienza dello spazio. Ma, ancora una volta, questo è un problema diverso, che idealmente dovrebbe essere affrontato dal compilatore.

Osservazioni finali

È abbastanza chiaro, dai commenti o dalle altre risposte, che i linguaggi di programmazione funzionale funzionale non sono puramente funzionali. Ciò riflette il fatto che la nostra tecnologia deve ancora essere migliorata, soprattutto per quanto riguarda la compilazione.

È possibile scrivere in uno stile puramente applicativo? La risposta è nota da circa 40 anni ed è "sì". Lo scopo stesso della semantica denotazionale come appariva negli anni '70 era proprio quello di tradurre (compilare) le lingue in uno stile puramente funzionale, ritenuto meglio compreso matematicamente e quindi considerato un migliore fondo per definire la semantica dei programmi.

L'aspetto interessante è che la struttura di programmazione imperativa, comprese le variabili, può essere tradotta in uno stile funzionale introducendo domini di valori appropriati, come un archivio dati. E nonostante lo stile funzionale, rimane sorprendentemente simile al codice dei compilatori reali scritti in stile imperativo.


0

È un'idea sbagliata che i programmi funzionali non possano archiviare i dati, e non credo che la risposta di Jakes lo abbia spiegato molto bene.

I programmi funzionali sono, come tutti i programmi, funzioni che mappano numeri interi a numeri interi. Qualsiasi programma imperativo che opera su strutture di dati mutabili ha una controparte funzionale. Questo è solo un altro mezzo per raggiungere lo stesso fine.

Il modo funzionale di archiviare i dati dell'esperimento da qualche fonte sarebbe chiamare la funzione di memorizzazione con la struttura dei dati come argomento e produrre una concatenazione della struttura di dati esistente e dei nuovi dati e quindi i dati vengono archiviati senza la nozione di strutture di dati mutabili.

Dalla mia esperienza , penso che il concetto di strutture di dati immutabili stia portando gli sviluppatori convenzionali a pensare che ci siano alcune cose che sono poco pratiche o addirittura impossibili da fare in un ambiente funzionale. Questo non è il caso.


"I programmi funzionali sono, come tutti i programmi, funzioni che mappano numeri interi a numeri interi." Come è, diciamo, Minecraft, davvero una funzione che mappa numeri interi a numeri interi?
David Richerby,

Facilmente. Ogni byte può essere interpretato come un intero binario. Uno stato in un computer è una raccolta di byte. Un programma, anche Minecraft, manipola lo stato di un computer, mappandolo da uno stato all'altro.
Jeppe Hartmund,

L'input dell'utente non sembra adattarsi a questo mondo.
David Richerby,

L'input dell'utente fa parte di uno stato di computer. Non esiste solo sul tuo schermo.
Jeppe Hartmund,
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.