Haskell Language
Linse
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
- Extrahieren Sie das
a
indem Sie den zusätzlichen Kontext oder vergessen - 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.
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 derSimple 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" dasa
aus dems
-
set :: Lens' sa -> (a -> s -> s)
"setzt" dena
Slot ins
-
review :: Prism' sa -> (a -> s)
"erkennt", dass eina
eins
-
preview :: Prism' sa -> (s -> Maybe a)
"versucht", aus einems
eina
.
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
vonLens
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.