2012-05-28 8 views
33

Ich kann anscheinend keine Erklärung dafür finden, welche Linsen in praktischen Beispielen verwendet werden. Dieser kurze Absatz von der Hackage-Seite ist der nächste, den ich gefunden habe:Wofür werden Linsen verwendet?

Diese Module bieten eine bequeme Möglichkeit, auf die Elemente einer Struktur zuzugreifen und diese zu aktualisieren. Es ist Data.Accessors sehr ähnlich, aber etwas generischer und hat weniger Abhängigkeiten. Ich mag besonders, wie sauber es verschachtelte Strukturen in Zustands-Monaden handhabt.

Also, wofür werden sie verwendet? Welche Vor- und Nachteile haben sie gegenüber anderen Methoden? Warum werden sie benötigt?

+2

Sie könnten gerne Edward Kmetts [Objektive: Ein funktionaler Imperativ] (http://www.youtube.com/watch?v=efv0SQNde5Q) sprechen. Es wird in Scala vorgestellt, aber die Übersetzung zur Nützlichkeit von Linsen in Haskell sollte klar sein. –

Antwort

45

Sie bieten eine saubere Abstraktion über Datenaktualisierungen und werden nie wirklich "benötigt". Sie lassen dich nur auf andere Weise über ein Problem nachdenken. In einigen imperativen/"objektorientierten" Programmiersprachen wie C haben Sie das vertraute Konzept einer Sammlung von Werten (nennen wir sie "Strukturen") und Möglichkeiten, jeden Wert in der Sammlung zu beschriften (die Beschriftungen sind typisch) "Felder" genannt).Dies führt zu einer Definition wie folgt:

typedef struct { /* defining a new struct type */ 
    float x; /* field */ 
    float y; /* field */ 
} Vec2; 

typedef struct { 
    Vec2 col1; /* nested structs */ 
    Vec2 col2; 
} Mat2; 

Sie können dann Werte dieser neu definierten Typs erzeugen etwa so:

Vec2 vec = { 2.0f, 3.0f }; 
/* Reading the components of vec */ 
float foo = vec.x; 
/* Writing to the components of vec */ 
vec.y = foo; 

Mat2 mat = { vec, vec }; 
/* Changing a nested field in the matrix */ 
mat.col2.x = 4.0f; 

Ähnlich in Haskell, haben wir Datentypen:

data Vec2 = 
    Vec2 
    { vecX :: Float 
    , vecY :: Float 
    } 

data Mat2 = 
    Mat2 
    { matCol1 :: Vec2 
    , matCol2 :: Vec2 
    } 

Dieser Datentyp wird dann wie folgt verwendet:

let vec = Vec2 2 3 
    -- Reading the components of vec 
    foo = vecX vec 
    -- Creating a new vector with some component changed. 
    vec2 = vec { vecY = foo } 

    mat = Mat2 vec2 vec2 

In Haskell gibt es jedoch keine einfache Möglichkeit, verschachtelte Felder in einer Datenstruktur zu ändern. Dies liegt daran, dass Sie alle Wrapping-Objekte um den Wert, den Sie ändern, neu erstellen müssen, da Haskell-Werte unveränderlich sind. Wenn Sie eine Matrix wie die oben in Haskell haben, und wollen die rechte obere Zelle in der Matrix ändern, müssen Sie schreiben:

mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } } 

Es funktioniert, aber es sieht unbeholfen. Also, was jemand vorhatte, ist im Grunde folgendes: Wenn man zwei Dinge zusammenfasst: den "Getter" eines Wertes (wie vecX und matCol2 oben) mit einer entsprechenden Funktion, die bei der Datenstruktur, zu der der Getter gehört, kann Erstellen Sie eine neue Datenstruktur mit diesem Wert geändert, Sie können eine Menge nette Sachen tun. Zum Beispiel:

data Data = Data { member :: Int } 

-- The "getter" of the member variable 
getMember :: Data -> Int 
getMember d = member d 

-- The "setter" or more accurately "updater" of the member variable 
setMember :: Data -> Int -> Data 
setMember d m = d { member = m } 

memberLens :: (Data -> Int, Data -> Int -> Data) 
memberLens = (getMember, setMember) 

Es gibt viele Möglichkeiten, Objektive zu implementieren; für diesen Text, lassen Sie uns sagen, dass eine Linse wie die oben genannten ist:

type Lens a b = (a -> b, a -> b -> a) 

I.e. es ist die Kombination eines Getters und eines Setter für irgendeinen Typ a, der ein Feld vom Typ b hat, also oben wäre ein Lens Data Int. Was lässt uns das machen?

Nun, lassen Sie uns zuerst zwei einfache Funktionen machen, die die Getter und Setter aus einer Linse extrahieren:

getL :: Lens a b -> a -> b 
getL (getter, setter) = getter 

setL :: Lens a b -> a -> b -> a 
setL (getter, setter) = setter 

Jetzt können wir abstrahieren über Sachen beginnen. Nehmen wir die obige Situation nochmals, dass wir einen Wert "zwei Stockwerke tief" ändern wollen. Wir fügen eine Datenstruktur mit einem anderen Objektiv:

data Foo = Foo { subData :: Data } 

subDataLens :: Lens Foo Data 
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition 

Jetzt wollen wir eine Funktion hinzufügen, die zwei Linsen komponiert:

(#) :: Lens a b -> Lens b c -> Lens a c 
(#) (getter1, setter1) (getter2, setter2) = 
    (getter2 . getter1, combinedSetter) 
    where 
     combinedSetter a x = 
     let oldInner = getter1 a 
      newInner = setter2 oldInner x 
     in setter1 a newInner 

Der Code Art ist schnell geschrieben, aber ich denke, es ist klar, was es tut, : Die Getter sind einfach zusammengesetzt; Sie erhalten den inneren Datenwert, und dann lesen Sie sein Feld. Wenn der Setter einen Wert a mit dem neuen inneren Feldwert x ändern soll, ruft er zuerst die alte innere Datenstruktur ab, legt sein inneres Feld fest und aktualisiert dann die äußere Datenstruktur mit der neuen inneren Datenstruktur.

Nun lassen Sie uns eine Funktion machen, die einfach den Wert einer Linse erhöht:

increment :: Lens a Int -> a -> a 
increment l a = setL l a (getL l a + 1) 

Wenn wir diesen Code haben, wird klar, was sie tut:

d = Data 3 
print $ increment memberLens d -- Prints "Data 4", the inner field is updated. 

Jetzt, da wir Objektive können wir auch zusammensetzen:

f = Foo (Data 5) 
print $ increment (subDataLens#memberLens) f 
-- Prints "Foo (Data 6)", the innermost field is updated. 

Was alle Linsenpakete machen, ist im Wesentlichen w rap dieses Konzept der Linsen - die Gruppierung eines "Setter" und eines "Getter", in ein ordentliches Paket, das sie einfach zu bedienen macht. Bei einer bestimmten Implementierung Linse wäre man in der Lage sein, zu schreiben:

with (Foo (Data 5)) $ do 
    subDataLens . memberLens $= 7 

Also, Sie auf die C-Version des Codes sehr nahe kommen; Es wird sehr einfach, verschachtelte Werte in einer Baumstruktur von Datenstrukturen zu ändern.

Objektive sind nichts anderes als eine einfache Möglichkeit, Teile von Daten zu verändern. Weil es so viel einfacher ist, über bestimmte Konzepte aufgrund von ihnen zu denken, werden sie in Situationen, in denen Sie riesige Mengen von Datenstrukturen haben, die auf verschiedene Weise miteinander interagieren müssen, breit eingesetzt.

Für die Vor- und Nachteile von Objektiven, siehe a recent question here on SO.

+2

Ein wichtiger Punkt, den deine Antwort vermisst, ist, dass Linsen * erste Klasse * sind, also kannst du andere Abstraktionen daraus machen. Die integrierte Datensatzsyntax schlägt in dieser Hinsicht fehl. – jberryman

+2

Außerdem schrieb ich einen Blogbeitrag über Objektive, die für das OP nützlich sein könnten: http://www.haskellforall.com/2012/01/haskell-for-mainstream-programmers_28.html –

12

Linsen bieten bequeme Möglichkeiten, bearbeiten Datenstrukturen, in einer einheitlichen, kompositorischen Art und Weise.

Viele Programme sind um die folgenden Operationen gebaut:

  • Betrachten einer Komponente einer (möglicherweise verschachtelte) Datenstruktur
  • Aktualisierung Felder (möglicherweise verschachtelte) Datenstrukturen

Objektive bieten Sprachunterstützung für das Anzeigen und Bearbeiten von Strukturen in einer Weise, die gewährleistet, dass Ihre Änderungen konsistent sind; dass Bearbeitungen leicht zusammengesetzt werden können; und dass derselbe Code zum Betrachten von Teilen einer Struktur verwendet werden kann, wie zum Aktualisieren der Teile der Struktur.

Linsen machen es so einfach, Programme von Ansichten auf Strukturen zu schreiben; und von Strukturen zurück zu Ansichten (und Editoren) für diese Strukturen. Sie bereinigen eine Menge des Durcheinanders von Plattenaccessoren und -einstellern.

Pierce et al. popularisierte Linsen, z.B. in ihrer Quotient Lenses paper und Implementierungen für Haskell sind jetzt weit verbreitet (z. B. fclabels und Daten-Accessoren).

Für Anwendungsfälle konkret betrachten:

  • grafische Benutzeroberflächen, in dem ein Benutzerinformation in einer strukturierten Art und Weise der Bearbeitung wird
  • Parser und ziemlich Drucker
  • Compiler
  • Synchronisationsdatenstrukturen Aktualisierung
  • Datenbanken und Schemas

und viele andere Situationen, in denen Sie ein Datenstrukturmodell der Welt und eine editierbare Sicht auf diese Daten haben.

6

Als zusätzliche Anmerkung wird oft übersehen, dass Objektive einen sehr allgemeinen Begriff von "Feldzugriff und -aktualisierung" implementieren. Objektive können für alle Arten von Dingen geschrieben werden, einschließlich funktionsähnlicher Objekte. Es erfordert ein wenig abstrakten Denken, dies zu schätzen, so lassen Sie mich Ihnen ein Beispiel für die Macht der Linsen zeigen:

at :: (Eq a) => a -> Lens (a -> b) b 

at Verwendung können Sie tatsächlich Zugriff und Manipulation Funktionen mit mehreren Argumenten zu früheren Argumenten abhängig. Denken Sie daran, dass Lens eine Kategorie ist. Dies ist ein sehr nützliches Idiom zum lokalen Anpassen von Funktionen oder anderen Dingen.

können Sie auch Zugriff auf Daten, die durch Eigenschaften oder alternative Darstellungen:

polar :: (Floating a, RealFloat a) => Lens (Complex a) (a, a) 
mag :: (RealFloat a) => Lens (Complex a) a 

Sie weitere Linsen Schreiben gehen können mehr einzelne Bänder eines Fourier-transformierten Signals und viel zuzugreifen.

Verwandte Themen