Le chiusure sono considerate stile funzionale impuro?


33

Le chiusure sono considerate impure nella programmazione funzionale?

Sembra che si possa generalmente evitare la chiusura passando i valori direttamente a una funzione. Pertanto le chiusure dovrebbero essere evitate ove possibile?

Se sono impuri e ho ragione nel dichiarare che possono essere evitati, perché così tanti linguaggi di programmazione funzionale supportano le chiusure?

Uno dei criteri per una funzione pura è che "La funzione valuta sempre lo stesso valore di risultato dati gli stessi valori di argomento ".

supporre

f: x -> x + y

f(3)non darà sempre lo stesso risultato. f(3)dipende dal valore di ycui non è un argomento f. Quindi fnon è una funzione pura.

Poiché tutte le chiusure si basano su valori che non sono argomenti, come è possibile che una chiusura sia pura? Sì, in teoria il valore chiuso potrebbe essere costante, ma non c'è modo di saperlo solo osservando il codice sorgente della funzione stessa.

Dove questo mi porta è che la stessa funzione può essere pura in una situazione ma impura in un'altra. Non si può sempre determinare se una funzione è pura o no studiando il suo codice sorgente. Piuttosto, potrebbe essere necessario considerarlo nel contesto del suo ambiente nel momento in cui viene chiamato prima che tale distinzione possa essere fatta.

Ci sto pensando correttamente?


6
Uso chiusure continuamente in Haskell, e Haskell è puro come si arriva.
Thomas Eding,

5
In un linguaggio funzionale puro, ynon può cambiare, quindi l'output di f(3)sarà sempre lo stesso.
Lily Chung,

4
yfa parte della definizione di fanche se non è esplicitamente contrassegnato come input per f- è ancora il caso che fè definito in termini di y(potremmo denotare la funzione f_y, per rendere yesplicita la dipendenza da )), e quindi cambiare ydà una funzione diversa . La funzione particolare f_ydefinita per un particolare yè molto pura. (Ad esempio, le due funzioni f: x -> x + 3e f: x -> x + 5sono funzioni diverse , ed entrambe pure, anche se ci è capitato di usare la stessa lettera per
denotarle

Risposte:


26

La purezza può essere misurata da due cose:

  1. La funzione restituisce sempre lo stesso output, dato lo stesso input? cioè è referenzialmente trasparente?
  2. La funzione modifica qualcosa al di fuori di se stessa, ovvero ha effetti collaterali?

Se la risposta a 1 è sì e la risposta a 2 è no, la funzione è pura. Le chiusure rendono impura una funzione solo se si modifica la variabile chiusa.


Il determinismo del primo articolo non è? O fa anche parte della purezza? Non ho familiarità con il concetto di "purezza" nel contesto della programmazione.

4
@JimmyHoffa: non necessariamente. È possibile inserire l'output di un timer hardware in una funzione e nulla al di fuori della funzione viene modificato.
Robert Harvey,

1
@RobertHarvey Riguarda il modo in cui definiamo gli input per una funzione? La mia citazione da Wikipedia si concentra sugli argomenti delle funzioni, mentre si considerano inoltre le variabili chiuse come input.
user2179977

8
@ user2179977: a meno che non siano mutabili, non considerare le variabili chiuse come input aggiuntivi per la funzione. Piuttosto, dovresti considerare la chiusura stessa come una funzione e una funzione diversa quando si chiude su un valore diverso di y. Quindi, ad esempio, definiamo una funzione gtale che g(y)è essa stessa la funzione x -> x + y. Quindi gè una funzione di numeri interi che restituisce funzioni, g(3)è una funzione di numeri interi che restituisce numeri interi ed g(2)è una diversa funzione di numeri interi che restituisce numeri interi. Tutte e tre le funzioni sono pure.
Steve Jessop,

1
@Darkhogg: Sì. Vedi il mio aggiornamento
Robert Harvey,

10

Le chiusure appaiono Lambda Calculus, che è la forma più pura di programmazione funzionale possibile, quindi non le definirei "impure" ...

Le chiusure non sono "impure" perché le funzioni nei linguaggi funzionali sono cittadini di prima classe, il che significa che possono essere trattati come valori.

Immagina questo (pseudocodice):

foo(x) {
    let y = x + 1
    ...
}

yè un valore. Il suo valore dipende x, ma xè immutabile, quindi anche yil valore è immutabile. Possiamo chiamare foomolte volte con argomenti diversi che produrranno ys diversi , ma quelli ys vivono tutti in ambiti diversi e dipendono da differentix s , quindi la purezza rimane intatta.

Ora cambiamolo:

bar(x) {
    let y(z) = x + z
    ....
}

Qui stiamo usando una chiusura (stiamo chiudendo su x), ma è esattamente la stessa di in foo- chiamate bardiverse con argomenti diversi creano valori diversi di y(ricorda - le funzioni sono valori) che sono tutti immutabili, quindi la purezza rimane intatta.

Inoltre, tieni presente che le chiusure hanno un effetto molto simile al curry:

adder(a)(b) {
    return a + b
}
baz(x) {
    let y = adder(x)
    ...
}

baznon è molto diverso da bar- in entrambi creiamo un valore di funzione chiamato yche restituisce il suo argomento plus x. In effetti, in Lambda Calculus usi le chiusure per creare funzioni con più argomenti - e non è ancora impuro.


9

Altri hanno ben coperto la domanda generale nelle loro risposte, quindi cercherò solo di chiarire la confusione che segnali nella tua modifica.

La chiusura non diventa un input della funzione, ma "entra" nel corpo della funzione. Per essere più concreti, una funzione si riferisce a un valore nell'ambito esterno nel suo corpo.

Hai l'impressione che renda impura la funzione. Questo non è il caso, in generale. Nella programmazione funzionale, i valori sono immutabili per la maggior parte del tempo . Ciò vale anche per il valore chiuso.

Diciamo che hai un pezzo di codice come questo:

let make y =
    fun x -> x + y

Chiamare make 3e make 4vi darà due funzioni con chiusure oltre make's yargomento. Uno tornerà x + 3, l'altro x + 4. Sono tuttavia due funzioni distinte ed entrambe sono pure. Sono stati creati usando la stessa makefunzione, ma il gioco è fatto.

Nota il più delle volte alcuni paragrafi indietro.

  1. In Haskell, che è puro, puoi solo chiudere valori immutabili. Non esiste uno stato mutabile da chiudere. Sei sicuro di ottenere una funzione pura in quel modo.
  2. Nei linguaggi funzionali impuri, come F #, puoi chiudere le celle di riferimento e i tipi di riferimento e ottenere una funzione impura. Hai ragione nel dover tracciare l'ambito entro il quale è definita la funzione per sapere se è pura o no. Puoi facilmente capire se un valore è mutabile in quelle lingue, quindi non è un grosso problema.
  3. Nei linguaggi OOP che supportano le chiusure, come C # e JavaScript, la situazione è simile ai linguaggi funzionali impuri, ma il monitoraggio dell'ambito esterno diventa più complicato poiché le variabili sono modificabili per impostazione predefinita.

Si noti che per 2 e 3, queste lingue non offrono alcuna garanzia sulla purezza. L'impurità non è una proprietà della chiusura, ma della lingua stessa. Le chiusure non cambiano molto l'immagine da sole.


1
Puoi assolutamente chiudere i valori mutabili in Haskell, ma una cosa del genere verrebbe annotata con la monade IO.
Daniel Gratzer,

1
@jozefg no, chiudi un IO Avalore immutabile e il tuo tipo di chiusura è IO (B -> C)o qualcosa del genere. La purezza è mantenuta
Caleth il

5

Normalmente ti chiedo di chiarire la tua definizione di "impuro", ma in questo caso non importa. Supponendo che lo si contrapponga al termine puramente funzionale , la risposta è "no", perché non c'è nulla nelle chiusure intrinsecamente distruttive. Se la tua lingua fosse puramente funzionale senza chiusure, sarebbe comunque puramente funzionale con chiusure. Se invece intendi "non funzionale", la risposta è ancora "no"; le chiusure facilitano la creazione di funzioni.

Sembra che si possa generalmente evitare la chiusura passando i dati direttamente a una funzione.

Sì, ma la tua funzione avrebbe un altro parametro e questo cambierebbe il suo tipo. Le chiusure consentono di creare funzioni basate su variabili senza aggiungere parametri. Ciò è utile quando si dispone, ad esempio, di una funzione che accetta 2 argomenti e si desidera crearne una versione che accetta solo 1 argomento.

EDIT: per quanto riguarda la tua modifica / esempio ...

supporre

f: x -> x + y

f (3) non darà sempre lo stesso risultato. f (3) dipende dal valore di y che non è un argomento di f. Quindi f non è una funzione pura.

Dipende dalla scelta sbagliata della parola qui. Citando lo stesso articolo di Wikipedia che hai fatto:

Nella programmazione al computer, una funzione può essere descritta come una funzione pura se entrambe queste affermazioni sulla funzione contengono:

  1. La funzione valuta sempre lo stesso valore di risultato dati gli stessi valori di argomento. Il valore del risultato della funzione non può dipendere da alcuna informazione o stato nascosto che può cambiare man mano che procede l'esecuzione del programma o tra diverse esecuzioni del programma, né può dipendere da alcun input esterno dai dispositivi I / O.
  2. La valutazione del risultato non provoca alcun effetto collaterale o output osservabile semanticamente, come la mutazione di oggetti mutabili o l'output a dispositivi I / O.

Supponendo che ysia immutabile (come di solito accade nei linguaggi funzionali), la condizione 1 è soddisfatta: per tutti i valori di x, il valore di f(x)non cambia. Ciò dovrebbe essere chiaro dal fatto che ynon è diverso da una costante ed x + 3è puro. È anche chiaro che non ci sono mutazioni o I / O in corso.


3

Molto rapidamente: una sostituzione è "referenzialmente trasparente" se "la sostituzione di like porta a like" e una funzione è "pura" se tutti i suoi effetti sono contenuti nel suo valore di ritorno. Entrambi possono essere resi precisi, ma è fondamentale notare che non sono identici né implicano nemmeno l'uno.

Ora parliamo di chiusure.

"Chiusure" noiose (per lo più pure)

Le chiusure si verificano perché quando valutiamo un termine lambda interpretiamo le variabili (associate) come ricerche di ambiente. Pertanto, quando restituiamo un termine lambda come risultato di una valutazione, le variabili al suo interno avranno "chiuso" i valori che hanno assunto quando è stato definito.

Nel semplice calcolo lambda questo è un po 'banale e l'intera nozione svanisce. Per dimostrarlo, ecco un interprete di calcolo lambda relativamente leggero:

-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)

-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions

type Name = String

data Expr
  = Var Name
  | App Expr Expr
  | Abs Name Expr

-- We model the environment as function from strings to values, 
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value

-- The empty environment
env0 :: Env
env0 _ = error "Nope!"

-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
                  | otherwise = e nm

-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name          -- variable lookup in the env
interp e (App ef ex) =
  let FunVal f = interp e ef
      x        = interp e ex
  in f x                              -- application to lambda terms
interp e (Abs name expr) =
  -- augmentation of a local (lexical) environment
  FunVal (\value -> interp (addEnv name value e) expr)

La parte importante da notare è addEnvquando aumentiamo l'ambiente con un nuovo nome. Questa funzione viene chiamata solo "dentro" del Abstermine di trazione interpretato (termine lambda). L 'ambiente viene "cercato" ogni volta che valutiamo un Vartermine e quindi quelli si Varrisolvono in qualunque cosa a Namecui si fa riferimento nella Envquale sia stata catturata dalla Abstrazione contenente il Var.

Ora, di nuovo, in termini semplici di LC questo è noioso. Significa che le variabili associate sono solo costanti per quanto interessa a chiunque. Vengono valutati direttamente e immediatamente come valori che denotano nell'ambiente in modo lessicale fino a quel momento.

Anche questo è (quasi) puro. L'unico significato di qualsiasi termine nel nostro calcolo lambda è determinato dal suo valore di ritorno. L'unica eccezione è l'effetto collaterale della non terminazione che è incarnato dal termine Omega:

-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x") 
                          (Var "x")))
            (Abs "x" (App (Var "x") 
                          (Var "x")))

Chiusure (impure) interessanti

Ora, per alcuni sfondi, le chiusure descritte nella LC di cui sopra sono noiose perché non si ha la possibilità di interagire con le variabili su cui abbiamo chiuso. In particolare, la parola "chiusura" tende a invocare il codice come il seguente Javascript

> function mk_counter() {
  var n = 0;
  return function incr() {
    return n += 1;
  }
}
undefined

> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3

Ciò dimostra che abbiamo chiuso la nvariabile nella funzione interna incre che la chiamata incrinteragisce in modo significativo con quella variabile. mk_counterè puro, ma incrè decisamente impuro (e neanche referenzialmente trasparente).

Cosa differisce tra questi due casi?

Nozioni di "variabile"

Se guardiamo al significato di sostituzione e astrazione in senso lato LC, notiamo che sono decisamente chiari. Le variabili non sono altro che ricerche di ambiente immediate. L'astrazione lambda non è letteralmente altro che la creazione di un ambiente aumentato per valutare l'espressione interiore. Non c'è spazio in questo modello per il tipo di comportamento che abbiamo visto con mk_counter/ incrperché non sono consentite variazioni.

Per molti questo è il cuore di ciò che significa "variabile": variazione. Tuttavia, ai semantisti piace distinguere tra il tipo di variabile utilizzata in LC e il tipo di "variabile" utilizzata in Javascript. Per fare ciò, tendono a definire quest'ultima una "cella mutabile" o "slot".

Questa nomenclatura segue il lungo uso storico di "variabile" in matematica dove significava qualcosa di più simile a "sconosciuto": l'espressione (matematica) "x + x" non consente xdi variare nel tempo, ma deve avere significato indipendentemente del valore (singolo, costante)x accetta.

Quindi, diciamo "slot" per enfatizzare la capacità di inserire valori in uno slot e di eliminarli.

Per aggiungere ulteriore confusione, in Javascript questi "slot" hanno lo stesso aspetto delle variabili: scriviamo

var x;

per crearne uno e poi quando lo scriviamo

x;

ci indica che stiamo cercando il valore attualmente memorizzato in quello slot. Per renderlo più chiaro, i linguaggi puri tendono a pensare alle slot come a prendere nomi come nomi (matematici, lambda calculus). In questo caso dobbiamo esplicitamente etichettare quando otteniamo o mettiamo da uno slot. Tale notazione tende ad apparire

-- create a fresh, empty slot and name it `x` in the context of the 
-- expression E
let x = newSlot in E

-- look up the value stored in the named slot named `x`, return that value
get x

-- store a new value, `v`, in the slot named `x`, return the slot
put x v

Il vantaggio di questa notazione è che ora abbiamo una netta distinzione tra variabili matematiche e slot mutabili. Le variabili possono assumere gli slot come valori, ma lo slot particolare indicato da una variabile è costante in tutto il suo ambito.

Usando questa notazione possiamo riscrivere l' mk_counteresempio (questa volta in una sintassi simile a Haskell, sebbene decisamente semantica non simile a Haskell):

mkCounter = 
  let x = newSlot 
  in (\() -> let old = get x 
             in get (put x (old + 1)))

In questo caso stiamo usando procedure che manipolano questo slot mutabile. Per implementarlo dovremmo chiudere non solo un ambiente costante di nomi comex ma anche un ambiente mutevole contenente tutti gli slot necessari. Questo è più vicino alla nozione comune di "chiusura" che la gente ama così tanto.

Ancora una volta, mkCounterè molto impuro. È anche molto referenzialmente opaco. Ma noti che gli effetti collaterali non derivano dalla cattura o dalla chiusura del nome ma invece dalla cattura della cellula mutabile e dalle operazioni di effetto collaterale su di essa come gete put.

In definitiva, penso che questa sia la risposta finale alla tua domanda: la purezza non è influenzata dall'acquisizione (matematica) di variabili ma invece da operazioni con effetti collaterali eseguite su slot mutabili denominati da variabili acquisite.

È solo quello in lingue che non tentano di avvicinarsi alla LC o non tentano di mantenere la purezza che questi due concetti sono così spesso confusi che portano alla confusione.


1

No, le chiusure non rendono impura una funzione, purché il valore chiuso sia costante (né modificato dalla chiusura né da altro codice), che è il solito caso nella programmazione funzionale.

Nota che mentre puoi sempre passare un valore come argomento, di solito non puoi farlo senza una considerevole difficoltà. Ad esempio (coffeescript):

closedValue = 42
return (arg) -> console.log "#{closedValue} #{arg}"

Su tuo suggerimento, potresti semplicemente restituire:

return (arg, closedValue) -> console.log "#{closedValue} #{arg}"

Questa funzione non viene chiamata a questo punto, appena definita , quindi dovresti trovare un modo per passare il valore desiderato closedValueal punto in cui viene effettivamente chiamata la funzione. Nella migliore delle ipotesi questo crea molto accoppiamento. Nel peggiore dei casi, non controlli il codice nel punto di chiamata, quindi è effettivamente impossibile.

Le librerie di eventi in lingue che non supportano le chiusure di solito forniscono un altro modo per restituire dati arbitrari al callback, ma non è carino e crea molta complessità sia per il manutentore della libreria che per gli utenti della libreria.

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.