Suche…


Einführung

Lens ist eine Bibliothek für Haskell, die Linsen, Isomorphismen, Falten, Traversierungen, Getter und Setter bereitstellt, die eine einheitliche Schnittstelle für das Abfragen und Manipulieren beliebiger Strukturen bietet, ähnlich wie bei den Accessor- und Mutator-Konzepten von Java.

Bemerkungen

Was ist eine Linse?

Objektive (und andere Optiken) erlauben uns zu beschreiben, wie wir auf einige Daten zugreifen wollen, von dem, was wir damit machen wollen. Es ist wichtig, zwischen dem abstrakten Begriff einer Linse und der konkreten Umsetzung zu unterscheiden. Abstraktes Verständnis macht das Programmieren mit dem lens auf lange Sicht viel einfacher. Es gibt viele isomorphe Darstellungen von Linsen, sodass wir in dieser Diskussion keine konkreten Implementierungsdiskussionen vermeiden und stattdessen einen allgemeinen Überblick über die Konzepte geben.

Fokussierung

Ein wichtiger Begriff für das abstrakte Verständnis ist der Begriff der Fokussierung . Wichtige Optiken konzentrieren sich auf einen bestimmten Teil einer größeren Datenstruktur, ohne den größeren Kontext zu vergessen. Beispielsweise fokussiert die Linse _1 auf das erste Element eines Tupels, vergisst jedoch nicht, was sich im zweiten Feld befand.

Sobald wir den Fokus haben, können wir darüber sprechen, welche Operationen wir mit einem Objektiv ausführen dürfen. In Anbetracht einer Lens sa , bei der ein Datentyp vom Typ s auf ein bestimmtes a fokussiert wird, können wir entweder

  1. Extrahieren Sie das a indem Sie den zusätzlichen Kontext oder vergessen
  2. Ersetzen Sie das a indem Sie einen neuen Wert angeben

Diese entsprechen den bekannten get und set Operationen, die normalerweise zur Charakterisierung einer Linse verwendet werden.

Andere Optiken

In ähnlicher Weise können wir über andere Optiken sprechen.

Optik Konzentriert sich auf...
Linse Ein Teil eines Produktes
Prisma Ein Teil einer Summe
Traversal Null oder mehr Teile einer Datenstruktur
Isomorphismus ...

Jede Optik fokussiert auf unterschiedliche Art und Weise, abhängig davon, mit welcher Art von Optik wir verschiedene Operationen ausführen können.

Zusammensetzung

Darüber hinaus können wir eine der beiden bisher besprochenen Optiken zusammenstellen, um komplexe Datenzugriffe zu spezifizieren. Die vier Arten von Optiken, die wir besprochen haben, bilden ein Gitter, das Ergebnis des Zusammenstellens zweier Optiken ist ihre obere Grenze.

Geben Sie hier die Bildbeschreibung ein

Wenn wir zum Beispiel eine Linse und ein Prisma zusammensetzen, erhalten wir eine Durchquerung. Der Grund dafür ist, dass wir uns bei ihrer (vertikalen) Zusammensetzung zunächst auf einen Teil eines Produkts und dann auf einen Teil einer Summe konzentrieren. Das Ergebnis ist eine Optik, die auf genau null oder einen Teil unserer Daten fokussiert, was ein Sonderfall einer Durchquerung ist. (Dies wird manchmal auch als affine Durchquerung bezeichnet).

In Haskell

Der Grund für die Popularität in Haskell ist, dass es eine sehr kurze Darstellung der Optik gibt. Alle Optiken sind nur Funktionen einer bestimmten Form, die durch Funktionszusammenstellung zusammengesetzt werden können. Dies führt zu einer sehr leichten Einbettung, die es leicht macht, Optiken in Ihre Programme zu integrieren. Darüber hinaus berechnet die Funktionszusammensetzung aufgrund der Besonderheiten der Kodierung automatisch auch die Obergrenze zweier von uns zusammengestellter Optiken. Dies bedeutet, dass wir dieselben Kombinatoren für unterschiedliche Optiken ohne explizites Gießen wiederverwenden können.

Tupel mit dem Objektiv bearbeiten

Bekommen

("a", 1) ^. _1 -- returns "a"
("a", 1) ^. _2 -- returns 1

Rahmen

("a", 1) & _1 .~ "b" -- returns ("b", 1)

Ändern

("a", 1) & _2 %~ (+1) -- returns ("a", 2)

both Traversal

(1, 2) & both *~ 2 -- returns (2, 4)

Objektive für Datensätze

Einfacher Rekord

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

data Point = Point {
    _x :: Float,
    _y :: Float
}
makeLenses ''Point

Objektive x und y werden erstellt.

let p = Point 5.0 6.0 
p ^. x     -- returns 5.0
set x 10 p -- returns Point { _x = 10.0, _y = 6.0 }
p & x +~ 1 -- returns Point { _x = 6.0, _y = 6.0 }

Datensätze mit sich wiederholenden Feldnamen verwalten

data Person = Person { _personName :: String }
makeFields ''Person

Erstellt eine Typklasse HasName , name für Person und macht Person eine Instanz von HasName . Nachfolgende Datensätze werden ebenfalls zur Klasse hinzugefügt:

data Entity = Entity { _entityName :: String }
makeFields ''Entity

Die Erweiterung von Template Haskell ist erforderlich, damit makeFields funktioniert. Technisch ist es durchaus möglich, die so hergestellten Objektive auf andere Weise zu erstellen, z. B. von Hand.

Stateful Objektive

Linsenoperatoren haben nützliche Varianten, die in zustandsbehafteten Kontexten arbeiten. Sie werden durch Ersetzen von ~ durch = im Operatornamen erhalten.

(+~) :: Num a => ASetter s t a a -> a -> s -> t
(+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()

Hinweis: Von den stateful-Varianten wird nicht erwartet, dass sie den Typ ändern. Daher haben sie die Unterschriften der Lens' oder der Simple Lens' .

& Ketten loswerden

Wenn Linsenoperationen verkettet werden müssen, sieht das oft so aus:

change :: A -> A
change a = a & lensA %~ operationA
             & lensB %~ operationB
             & lensC %~ operationC

Dies funktioniert dank der Assoziativität von & . Die stateful-Version ist jedoch klarer.

change a = flip execState a $ do
    lensA %= operationA
    lensB %= operationB
    lensC %= operationC

Wenn lensX tatsächlich ist id kann der gesamte Betrieb natürlich direkt nur ausgeführt werden , durch Anheben mit modify .

Imperativer Code mit strukturiertem Zustand

Nehmen wir diesen Beispielzustand an:

data Point = Point { _x :: Float, _y :: Float }
data Entity = Entity { _position :: Point, _direction :: Float }
data World = World { _entities :: [Entity] }

makeLenses ''Point
makeLenses ''Entity
makeLenses ''World

Wir können Code schreiben, der klassischen imperativen Sprachen ähnelt, während wir dennoch die Vorteile von Haskell nutzen können:

updateWorld :: MonadState World m => m ()
updateWorld = do
    -- move the first entity
    entities . ix 0 . position . x += 1

    -- do some operation on all of them
    entities . traversed . position %= \p -> p `pointAdd` ...

    -- or only on a subset
    entities . traversed . filtered (\e -> e ^. position.x > 100) %= ...

Ein Objektiv ohne Template Haskell schreiben

Um Haskell zu entmystifizieren, nehmen Sie an, Sie haben

data Example a = Example { _foo :: Int, _bar :: a }

dann

makeLenses 'Example

produziert (mehr oder weniger)

foo :: Lens' (Example a) Int
bar :: Lens (Example a) (Example b) a b

Es ist jedoch nichts besonders magisches los. Sie können diese selbst schreiben:

foo :: Lens' (Example a) Int
--  :: Functor f => (Int -> f Int) -> (Example a -> f (Example a))    ;; expand the alias
foo wrap (Example foo bar) = fmap (\newFoo -> Example newFoo bar) (wrap foo)

bar :: Lens (Example a) (Example b) a b
--  :: Functor f => (a -> f b) -> (Example a -> f (Example b))    ;; expand the alias
bar wrap (Example foo bar) = fmap (\newBar -> Example foo newBar) (wrap bar)

Im Wesentlichen möchten Sie mit der wrap "Fokus" Ihres Objektivs "besuchen" und anschließend den "gesamten" Typ neu erstellen.

Linse und Prisma

Ein Lens' sa bedeutet, dass Sie immer ein a innerhalb eines s . Eine Prism' sa bedeutet , dass Sie manchmal , dass finden s eigentlich nur a , aber manchmal ist es etwas anderes.

_1 :: Lens' (a, b) a haben wir _1 :: Lens' (a, b) a da jedes Tupel immer ein erstes Element hat. Wir haben _Just :: Prism' (Maybe a) a , weil manchmal Maybe a tatsächlich eine ist a in gewickelten Wert Just aber manchmal ist es Nothing .

Mit dieser Intuition können einige Standardkombinatoren parallel zueinander interpretiert werden

  • view :: Lens' sa -> (s -> a) "holt" das a aus dem s
  • set :: Lens' sa -> (a -> s -> s) "setzt" den a Slot in s
  • review :: Prism' sa -> (a -> s) "erkennt", dass ein a ein s
  • preview :: Prism' sa -> (s -> Maybe a) "versucht", aus einem s ein a .

Eine andere Möglichkeit, darüber nachzudenken, besteht darin, dass ein Wert des Typs Lens' sa zeigt, dass s die gleiche Struktur wie (r, a) für ein unbekanntes r . Auf der anderen Seite, Prism' sa zeigt , dass s die gleiche Struktur wie hat Either ra für einige r . Wir können diese vier Funktionen mit diesem Wissen oben schreiben:

-- `Lens' s a` is no longer supplied, instead we just *know* that `s ~ (r, a)`

view :: (r, a) -> a
view (r, a) = a

set :: a -> (r, a) -> (r, a)
set a (r, _) = (r, a)

-- `Prism' s a` is no longer supplied, instead we just *know* that `s ~ Either r a`

review :: a -> Either r a
review a = Right a

preview :: Either r a -> Maybe a
preview (Left _) = Nothing
preview (Right a) = Just a

Überquerungen

Ein Traversal' sa zeigt , dass s hat 0-to-many a s im Inneren.

toListOf :: Traversal' s a -> (s -> [a])

Jeder Typ t der automatisch Traversable ist, hat folgende traverse :: Traversal (ta) a .

Wir können ein Traversal , um alle diese a Werte Traversal oder abzubilden

> set traverse 1 [1..10]
[1,1,1,1,1,1,1,1,1,1]

> over traverse (+1) [1..10]
[2,3,4,5,6,7,8,9,10,11]

Ein f :: Lens' sa sagt, es gibt genau a in s . Ein g :: Prism' ab sagt, es gibt entweder 0 oder 1 b in a . Komponieren von f . g gibt uns ein Traversal' sb weil das folgende f und dann g zeigt, wie es 0-zu-1 b s in s .

Linsen komponieren

Wenn Sie ein f :: Lens' ab und ein g :: Lens' bc dann f . g ist ein Lens' ac durch f zuerst und dann g . Vor allem:

  • Objektive komponieren als Funktionen (sie wirklich nur sind Funktionen)
  • Wenn Sie an die view von Lens denken, scheint es, als würden Daten von links nach rechts fließen - dies könnte sich Ihrer normalen Intuition für die Funktionszusammenstellung entgegenwirken. Auf der anderen Seite sollte es sich natürlich anfühlen, wenn Sie daran denken . -Notation wie es in OO-Sprachen geschieht.

Mehr als nur Komponieren Lens mit Lens , (.) Kann fast jede zusammen verwendet werden „ Lens -ähnlichen“ zusammen geben. Es ist nicht immer leicht zu sehen, was das Ergebnis ist, da der Typ schwieriger zu folgen ist, aber Sie können das lens um es herauszufinden. Die Zusammensetzung x . y hat den Typ der kleinsten oberen Grenze der Typen von x und y in diesem Diagramm.

Noble Linsen

Neben der Standard - makeLenses Funktion zum Erzeugen von Lens es, Control.Lens.TH bietet auch die makeClassy Funktion. makeClassy hat den gleichen Typ und funktioniert im Wesentlichen genauso wie makeLenses , mit einem Unterschied. Wenn der Typ keine Argumente enthält, erstellt er nicht nur die Standardobjektive und Durchquerungen, sondern auch eine Klasse, die alle Datentypen beschreibt, die den Typ als Feld besitzen. Zum Beispiel

data Foo = Foo { _fooX, _fooY :: Int }
  makeClassy ''Foo

wird erzeugen

class HasFoo t where
   foo :: Simple Lens t Foo

instance HasFoo Foo where foo = id

fooX, fooY :: HasFoo t => Simple Lens t Int

Felder mit makeFields

(Dieses Beispiel wurde aus dieser StackOverflow-Antwort kopiert.)

Nehmen wir an, Sie haben verschiedene Datentypen, die alle ein Objektiv mit demselben Namen haben müssen, in diesem Fall die capacity . Das makeFields Slice erstellt eine Klasse, die dies ohne Namensraumkonflikte schafft.

{-# LANGUAGE FunctionalDependencies
           , MultiParamTypeClasses
           , TemplateHaskell
  #-}

module Foo
where

import Control.Lens

data Foo
  = Foo { fooCapacity :: Int }
  deriving (Eq, Show)
$(makeFields ''Foo)

data Bar
  = Bar { barCapacity :: Double }
  deriving (Eq, Show)
$(makeFields ''Bar)

Dann in ghci:

*Foo
λ let f = Foo 3
|     b = Bar 7
| 
b :: Bar
f :: Foo

*Foo
λ fooCapacity f
3
it :: Int

*Foo
λ barCapacity b
7.0
it :: Double

*Foo
λ f ^. capacity
3
it :: Int

*Foo
λ b ^. capacity
7.0
it :: Double

λ :info HasCapacity 
class HasCapacity s a | s -> a where
  capacity :: Lens' s a
    -- Defined at Foo.hs:14:3
instance HasCapacity Foo Int -- Defined at Foo.hs:14:3
instance HasCapacity Bar Double -- Defined at Foo.hs:19:3

Also , was es tatsächlich getan wird eine Klasse deklariert HasCapacity sa , in denen die Kapazität ist ein Lens' von s auf a ( a fixiert ist einmal s bekannt ist ). Es hat den Namen "Kapazität" ermittelt, indem der Name des Datentyps aus dem Feld entfernt wurde. Ich finde es angenehm, dass Sie weder für den Feldnamen noch für den Objektivnamen einen Unterstrich verwenden müssen, da die Record-Syntax manchmal das ist, was Sie wollen. Sie können makeFieldsWith und die verschiedenen lensRules verwenden, um verschiedene Optionen für die Berechnung der Objektivnamen zu erhalten.

Falls es hilfreich ist, verwenden Sie ghci -ddump-splices Foo.hs:

[1 of 1] Compiling Foo              ( Foo.hs, interpreted )
Foo.hs:14:3-18: Splicing declarations
    makeFields ''Foo
  ======>
    class HasCapacity s a | s -> a where
      capacity :: Lens' s a
    instance HasCapacity Foo Int where
      {-# INLINE capacity #-}
      capacity = iso (\ (Foo x_a7fG) -> x_a7fG) Foo
Foo.hs:19:3-18: Splicing declarations
    makeFields ''Bar
  ======>
    instance HasCapacity Bar Double where
      {-# INLINE capacity #-}
      capacity = iso (\ (Bar x_a7ne) -> x_a7ne) Bar
Ok, modules loaded: Foo.

Der erste Spleiß machte also die Klasse HasCapcity und fügte eine Instanz für Foo hinzu. Der zweite verwendete die vorhandene Klasse und machte eine Instanz für Bar.

Dies funktioniert auch, wenn Sie die HasCapcity Klasse aus einem anderen Modul importieren. makeFields kann der vorhandenen Klasse weitere Instanzen hinzufügen und Ihre Typen auf mehrere Module verteilen. Wenn Sie sie jedoch erneut in einem anderen Modul verwenden, in dem Sie die Klasse nicht importiert haben, wird eine neue Klasse (mit demselben Namen) erstellt, und Sie verfügen über zwei separate überlastete Objektive, die nicht kompatibel sind.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow