Werden Verschlüsse als unreiner Funktionsstil angesehen?

33

Werden Verschlüsse in der funktionalen Programmierung als unrein angesehen?

Es scheint, dass man Abschlüsse generell vermeiden kann, indem man Werte direkt an eine Funktion übergibt. Deshalb sollten Schließungen nach Möglichkeit vermieden werden?

Wenn sie unrein sind und ich zu Recht behaupte, dass sie vermieden werden können, warum unterstützen so viele funktionale Programmiersprachen Schließungen?

Eines der Kriterien für eine reine Funktion lautet: "Die Funktion wertet bei gleichen Argumentwerten immer den gleichen Ergebniswert aus ."

Annehmen

f: x -> x + y

f(3)wird nicht immer das gleiche Ergebnis geben. f(3)hängt davon ab, ywelcher Wert kein Argument von ist f. Also fist das keine reine Funktion.

Da alle Abschlüsse auf Werten beruhen, die keine Argumente sind, wie ist es möglich, dass ein Abschluss rein ist? Ja, theoretisch könnte der geschlossene Wert konstant sein, aber es gibt keine Möglichkeit, dies zu erkennen, wenn man nur den Quellcode der Funktion selbst betrachtet.

Was mich dazu führt, ist, dass dieselbe Funktion in einer Situation rein, in einer anderen jedoch unrein sein kann. Man kann nicht immer feststellen, ob eine Funktion rein ist oder nicht, indem man ihren Quellcode untersucht. Vielmehr muss man es möglicherweise zu dem Zeitpunkt, zu dem es aufgerufen wird, im Kontext seiner Umgebung betrachten, bevor eine solche Unterscheidung getroffen werden kann.

Denke ich richtig darüber nach?

user2179977
quelle
6
In Haskell verwende ich ständig Verschlüsse, und Haskell ist so rein wie es nur geht.
Thomas Eding
5
yKann sich in einer reinen funktionalen Sprache nicht ändern, sodass die Ausgabe von f(3)immer dieselbe ist.
Lily Chung
4
yist Teil der Definition von f, obwohl es nicht explizit als Eingabe für gekennzeichnet ist f- es ist immer noch der Fall, fder in Bezug auf definiert ist y(wir könnten die Funktion f_y bezeichnen, um die Abhängigkeit von yexplizit zu machen ), und daher ygibt das Ändern eine andere Funktion . Die f_yfür ein bestimmtes definierte bestimmte Funktion yist sehr rein. (Zum Beispiel der beiden Funktionen f: x -> x + 3und f: x -> x + 5sind verschiedene Funktionen, und beide rein, auch wenn wir die gleichen Buchstaben zu verwenden , passierten sie zu bezeichnen.)
ShreevatsaR

Antworten:

26

Die Reinheit kann an zwei Dingen gemessen werden:

  1. Gibt die Funktion bei gleicher Eingabe immer die gleiche Ausgabe zurück? dh ist es referenziell transparent?
  2. Ändert die Funktion etwas außerhalb von sich selbst, dh hat sie Nebenwirkungen?

Wenn die Antwort auf 1 Ja und die Antwort auf 2 Nein lautet, ist die Funktion rein. Closures machen eine Funktion nur dann unrein, wenn Sie die Closed-Over-Variable ändern.

Robert Harvey
quelle
Ist nicht der erste Punkt Determinismus? Oder gehört das auch zur Reinheit? Das Konzept der "Reinheit" im Kontext der Programmierung ist mir nicht allzu vertraut.
4
@ JimmyHoffa: Nicht unbedingt. Sie können den Ausgang eines Hardware-Timers in eine Funktion einfügen, und nichts außerhalb der Funktion wird geändert.
Robert Harvey
1
@RobertHarvey Geht es darum, wie wir Eingaben für eine Funktion definieren? Mein Zitat aus Wikipedia konzentriert sich auf Funktionsargumente, während Sie zusätzlich die geschlossenen Variablen als Eingaben betrachten.
user2179977
8
@ user2179977: es sei denn , sie wandelbar sind, dann sollten Sie nicht die geschlossenen Über Variablen als zusätzliche Eingänge für die Funktion prüfen. Sie sollten den Abschluss selbst vielmehr als eine Funktion und als eine andere Funktion betrachten, wenn er über einen anderen Wert von geschlossen wird y. So definieren wir zum Beispiel eine Funktion g, g(y)die selbst die Funktion ist x -> x + y. Dann gist eine Funktion von Ganzzahlen, die Funktionen zurückgibt, g(3)eine Funktion von Ganzzahlen, die Ganzzahlen zurückgibt, und g(2)eine andere Funktion von Ganzzahlen, die Ganzzahlen zurückgibt. Alle drei Funktionen sind rein.
Steve Jessop
1
@ Darkhogg: Ja. Siehe mein Update.
Robert Harvey
10

Closures erscheinen in Lambda Calculus, der reinsten Form der funktionalen Programmierung, also würde ich sie nicht als "unrein" bezeichnen ...

Verschlüsse sind nicht "unrein", weil Funktionen in funktionalen Sprachen erstklassige Bürger sind - das heißt, sie können als Werte behandelt werden.

Stellen Sie sich Folgendes vor (Pseudocode):

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

yist ein Wert. Ihr Wert hängt davon ab x, ist aber xunveränderlich, so dass yder Wert auch unveränderlich ist. Wir können fooviele Male mit unterschiedlichen Argumenten aufrufen, die unterschiedliche ys hervorbringen , aber diese yleben alle in unterschiedlichen Bereichen und hängen von unterschiedlichen xs ab, sodass die Reinheit intakt bleibt.

Jetzt ändern wir es:

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

Hier verwenden wir einen Closure (wir schließen über x), aber es ist genauso wie in foo- verschiedene Aufrufe barmit unterschiedlichen Argumenten erzeugen unterschiedliche Werte von y(Merke - Funktionen sind Werte), die alle unveränderlich sind, damit die Reinheit erhalten bleibt.

Bitte beachten Sie auch, dass Verschlüsse einen sehr ähnlichen Effekt auf das Currying haben:

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

bazist nicht wirklich anders als bar- in beiden Fällen erstellen wir einen Funktionswert mit dem Namen y, der das Argument plus zurückgibt x. Tatsächlich verwenden Sie in Lambda Calculus Closures, um Funktionen mit mehreren Argumenten zu erstellen - und es ist immer noch nicht unrein.

Idan Arye
quelle
9

Andere haben die allgemeine Frage in ihren Antworten gut behandelt, daher werde ich mich nur mit der Beseitigung der Verwirrung befassen, die Sie in Ihrer Bearbeitung signalisieren.

Der Abschluss wird nicht zu einer Eingabe der Funktion, sondern geht in den Funktionskörper. Genauer gesagt bezieht sich eine Funktion auf einen Wert im äußeren Bereich ihres Körpers.

Sie haben den Eindruck, dass die Funktion dadurch unrein wird. Dies ist im Allgemeinen nicht der Fall. Bei der funktionalen Programmierung sind die Werte die meiste Zeit unveränderlich . Dies gilt auch für den Closed-Over-Wert.

Angenommen, Sie haben einen Code wie diesen:

let make y =
    fun x -> x + y

Wenn Sie make 3und make 4aufrufen, erhalten Sie zwei Funktionen mit Abschlüssen über makedas yArgument. Einer von ihnen wird zurückkehren x + 3, der andere x + 4. Sie sind jedoch zwei verschiedene Funktionen und beide sind rein. Sie wurden mit der gleichen makeFunktion erstellt, aber das war's.

Beachten Sie die meiste Zeit einige Absätze zurück.

  1. In Haskell, das rein ist, können Sie nur unveränderliche Werte schließen. Es gibt keinen veränderlichen Zustand, den man schließen könnte. Auf diese Weise erhalten Sie mit Sicherheit eine reine Funktion.
  2. In unreinen funktionalen Sprachen wie F # können Sie Referenzzellen und Referenztypen schließen und eine unreine Funktion aktivieren. Sie haben Recht, dass Sie den Bereich verfolgen müssen, in dem die Funktion definiert ist, um zu wissen, ob sie rein ist oder nicht. Sie können leicht feststellen, ob ein Wert in diesen Sprachen veränderbar ist, sodass dies kein großes Problem darstellt.
  3. In OOP-Sprachen, die Closures wie C # und JavaScript unterstützen, ist die Situation ähnlich wie in unreinen funktionalen Sprachen, aber das Verfolgen des äußeren Bereichs wird schwieriger, da Variablen standardmäßig veränderbar sind.

Beachten Sie, dass diese Sprachen für 2 und 3 keine Reinheitsgarantie bieten. Die Verunreinigung dort ist keine Eigenschaft des Verschlusses, sondern der Sprache selbst. Verschlüsse verändern das Bild von sich aus nicht wesentlich.

scrwtp
quelle
1
Sie können in Haskell durchaus veränderbare Werte schließen, aber so etwas würde mit der E / A-Monade kommentiert.
Daniel Gratzer
1
@jozefg nein, Sie schließen über einen unveränderlichen IO AWert und Ihr Schließungstyp ist IO (B -> C)oder so. Reinheit wird aufrechterhalten
Caleth
5

Normalerweise würde ich Sie bitten, Ihre Definition von "unrein" zu präzisieren, aber in diesem Fall spielt es keine Rolle. Unter der Annahme, dass Sie den Begriff rein funktional gegenüberstellen , lautet die Antwort "nein", da es bei Verschlüssen nichts gibt, was von Natur aus destruktiv ist. Wenn Ihre Sprache ohne Abschlüsse rein funktional wäre, wäre sie mit Abschlüssen immer noch rein funktional. Wenn Sie stattdessen "nicht funktionsfähig" meinen, lautet die Antwort immer noch "nein". Verschlüsse erleichtern die Erstellung von Funktionen.

Es scheint, dass man Schließungen im Allgemeinen vermeiden kann, indem man Daten direkt an eine Funktion übergibt.

Ja, aber dann hätte Ihre Funktion einen weiteren Parameter, und das würde ihren Typ ändern. Mit Closures können Sie Funktionen basierend auf Variablen erstellen, ohne Parameter hinzuzufügen. Dies ist nützlich, wenn Sie beispielsweise eine Funktion haben, die 2 Argumente akzeptiert und eine Version davon erstellen möchten, die nur 1 Argument akzeptiert.

EDIT: In Bezug auf deine eigene Bearbeitung / Beispiel ...

Annehmen

f: x -> x + y

f (3) liefert nicht immer das gleiche Ergebnis. f (3) hängt vom Wert von y ab, der kein Argument von f ist. Somit ist f keine reine Funktion.

Hängt ist die falsche Wortwahl hier. Zitiert den gleichen Wikipedia-Artikel, den Sie geschrieben haben:

In der Computerprogrammierung kann eine Funktion als reine Funktion beschrieben werden, wenn beide Aussagen über die Funktion gelten:

  1. Die Funktion wertet bei gleichen Argumentwerten immer den gleichen Ergebniswert aus. Der Funktionsergebniswert kann weder von verborgenen Informationen oder Zuständen abhängen, die sich während der Programmausführung oder zwischen verschiedenen Programmausführungen ändern, noch von externen Eingaben von E / A-Geräten.
  2. Die Auswertung des Ergebnisses verursacht keine semantisch beobachtbaren Nebenwirkungen oder Ausgaben, wie z. B. Mutationen von veränderlichen Objekten oder Ausgaben an E / A-Geräte.

Vorausgesetzt, dass yunveränderlich ist (was normalerweise in funktionalen Sprachen der Fall ist), ist Bedingung 1 erfüllt: Für alle Werte xvon f(x)ändert sich der Wert von nicht. Dies sollte sich aus der Tatsache ergeben, dass yes sich nicht um eine Konstante handelt und dass sie x + 3rein ist. Es ist auch klar, dass es keine Mutation oder I / O gibt.

Doval
quelle
3

Sehr schnell: Eine Ersetzung ist "referenziell transparent", wenn "Ersetzen von Gleichem zu Gleichem führt", und eine Funktion ist "rein", wenn alle ihre Auswirkungen in ihrem Rückgabewert enthalten sind. Beide können präzisiert werden, aber es ist wichtig zu beachten, dass sie nicht identisch sind und auch nicht das eine das andere impliziert.

Sprechen wir jetzt über Schließungen.

Langweilige (meist reine) "Verschlüsse"

Closures treten auf, weil wir bei der Auswertung eines Lambda-Terms (gebundene) Variablen als Umgebungs-Lookups interpretieren. Wenn wir also einen Lambda-Term als Ergebnis einer Auswertung zurückgeben, haben die darin enthaltenen Variablen die Werte "überschritten", die sie bei ihrer Definition angenommen haben.

In der einfachen Lambda-Rechnung ist dies eine Art Trivialität, und die ganze Vorstellung verschwindet einfach. Um dies zu demonstrieren, ist hier ein relativ leichter Lambda-Kalkül-Interpreter:

-- 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)

Der wichtige Punkt, den Sie beachten müssen, ist, addEnvwenn Sie der Umgebung einen neuen Namen geben. Diese Funktion wird nur "innerhalb" des interpretierten AbsTraktionsbegriffs (Lambda-Terms) aufgerufen . Die Umwelt wird „nachgeschlagen“ , wann immer wir eine bewerten VarBegriff und so diejenigen Vars Entschlossenheit, unabhängig von der Namegenannten in der Envdie durch die gefangen wurde AbsTraktion enthält das Var.

Auch dies ist in einfachen LC-Begriffen langweilig. Dies bedeutet, dass gebundene Variablen für jeden nur Konstanten sind. Sie werden direkt und sofort als die Werte ausgewertet, die sie in der Umgebung als bis zu diesem Zeitpunkt lexikalisch gültig bezeichnen.

Das ist auch (fast) rein. Die einzige Bedeutung eines Begriffs in unserer Lambda-Rechnung wird durch seinen Rückgabewert bestimmt. Die einzige Ausnahme ist die Nebenwirkung der Kündigung, die der Begriff Omega verkörpert:

-- 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")))

Interessante (unreine) Verschlüsse

Vor dem Hintergrund einiger Hintergründe sind die in LC oben beschriebenen Abschlüsse langweilig, da keine Vorstellung davon besteht, mit den Variablen, die wir geschlossen haben, interagieren zu können. Insbesondere neigt das Wort "Closure" dazu, Code wie das folgende Javascript aufzurufen

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

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

Dies zeigt, dass wir die nVariable in der inneren Funktion geschlossen haben incrund der Aufruf incrsinnvoll mit dieser Variablen interagiert. mk_counterist rein, aber incrausgesprochen unrein (und auch nicht referenziell transparent).

Was unterscheidet sich zwischen diesen beiden Fällen?

Begriffe von "Variable"

Wenn wir uns ansehen, was Substitution und Abstraktion im einfachen LC-Sinne bedeuten, stellen wir fest, dass sie eindeutig schlicht sind. Variablen sind buchstäblich nichts anderes als unmittelbare Umgebungssuchvorgänge. Lambda-Abstraktion ist buchstäblich nichts anderes als die Schaffung einer erweiterten Umgebung zur Bewertung des inneren Ausdrucks. Es gibt keinen Platz in diesem Modell für die Art von Verhalten , das wir Säge mit mk_counter/ incrweil es keine Variation erlaubt.

Für viele ist dies das Herz dessen, was "Variable" bedeutet - Variation. Semantiker unterscheiden jedoch gerne zwischen der Art der in LC verwendeten Variablen und der Art der in Javascript verwendeten "Variablen". Um dies zu tun, neigen sie dazu, letztere "veränderbare Zelle" oder "Schlitz" zu nennen.

Diese Nomenklatur folgt der langen historischen Verwendung von "Variable" in der Mathematik, wo sie eher "Unbekannt" bedeutete: Der (mathematische) Ausdruck "x + x" lässt nicht zu, dass xer sich über die Zeit ändert , sondern soll unabhängig von der Bedeutung sein des (einzelnen, konstanten) Wertes xnimmt.

Wir sagen daher "slot", um die Fähigkeit hervorzuheben, Werte in einen Slot zu setzen und sie herauszunehmen.

Um die Verwirrung noch weiter zu verstärken, sehen diese "Slots" in Javascript genauso aus wie Variablen: Wir schreiben

var x;

zu erstellen und dann, wenn wir schreiben

x;

Es zeigt an, dass wir den aktuell in diesem Slot gespeicherten Wert nachschlagen. Um dies klarer zu machen, neigen reine Sprachen dazu, sich Slots als Namen (mathematisch, Lambda-Kalkül) vorzustellen. In diesem Fall müssen wir ausdrücklich beschriften, wann wir von einem Slot bekommen oder setzen. Eine solche Schreibweise sieht normalerweise so aus

-- 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

Der Vorteil dieser Notation besteht darin, dass wir jetzt fest zwischen mathematischen Variablen und veränderlichen Zeitnischen unterscheiden können. Variablen können Slots als ihre Werte annehmen, aber der bestimmte Slot, der von einer Variablen benannt wird, ist im gesamten Gültigkeitsbereich konstant.

Mit dieser Notation können wir das mk_counterBeispiel umschreiben (diesmal in einer Haskell-ähnlichen Syntax, wenn auch ausgesprochen un-Haskell-ähnlichen Semantik):

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

In diesem Fall verwenden wir Prozeduren, die diesen veränderlichen Slot manipulieren. Um es zu implementieren, müssten wir nicht nur eine konstante Umgebung mit Namen schließen, xsondern auch eine veränderbare Umgebung, die alle benötigten Slots enthält. Dies ist näher an der allgemeinen Vorstellung von "Schließung", die die Menschen so sehr lieben.

Auch hier mkCounterist sehr unrein. Es ist auch sehr referenziell undurchsichtig. Beachten Sie aber , dass die Nebenwirkungen ergeben sich nicht aus dem Namen Fang oder Schließung , sondern die Einnahme der wandelbaren Zelle und den Seiten bewirken Operationen darauf , wie getund put.

Letztendlich denke ich, dass dies die endgültige Antwort auf Ihre Frage ist: Die Reinheit wird nicht durch (mathematische) Variablenerfassung beeinflusst, sondern durch nebenwirkende Operationen, die an veränderlichen Slots durchgeführt werden, die von erfassten Variablen benannt werden.

Es ist nur so, dass in Sprachen, die nicht versuchen, LC nahe zu kommen oder Reinheit nicht aufrechtzuerhalten, diese beiden Konzepte so oft miteinander verschmelzen, dass Verwirrung herrscht.

J. Abrahamson
quelle
1

Nein, Abschlüsse bewirken keine Verunreinigung einer Funktion, solange der Wert für Abschlüsse konstant ist (weder durch den Abschluß noch durch einen anderen Code geändert), was bei der funktionalen Programmierung der Fall ist.

Beachten Sie, dass Sie zwar immer einen Wert als Argument übergeben können, dies jedoch in der Regel nicht ohne größere Schwierigkeiten möglich ist. Zum Beispiel (Coffeescript):

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

Nach Ihrem Vorschlag können Sie einfach zurückkehren:

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

Diese Funktion wird an dieser Stelle nicht aufgerufen, sondern nur definiert . Sie müssen also einen Weg finden, um den gewünschten Wert closedValuean die Stelle zu übergeben, an der die Funktion tatsächlich aufgerufen wird. Bestenfalls entsteht dadurch viel Kopplung. Im schlimmsten Fall steuern Sie den Code nicht am aufrufenden Punkt, sodass dies praktisch unmöglich ist.

Ereignisbibliotheken in Sprachen, die Schließungen nicht unterstützen, bieten normalerweise eine andere Möglichkeit, beliebige Daten an den Rückruf zurückzuleiten. Sie sind jedoch nicht besonders ansprechend und verursachen sowohl für den Bibliotheksverwalter als auch für die Bibliotheksbenutzer eine große Komplexität.

Karl Bielefeldt
quelle