563 lines
15 KiB
Markdown
563 lines
15 KiB
Markdown
# Lenses
|
|
|
|
## Wofür brauchen wir das überhaupt?
|
|
|
|
Die Idee dahinter ist, dass man Zugriffsabstraktionen über Daten verknüpfen
|
|
kann. Als einfachen Datenstruktur kann man einen Record mit der entsprechenden
|
|
Syntax nehmen.
|
|
|
|
### Beispiel
|
|
|
|
~~~ { .haskell }
|
|
data Person = P { name :: String
|
|
, addr :: Address
|
|
, salary :: Int }
|
|
data Address = A { road :: String
|
|
, city :: String
|
|
, postcode :: String }
|
|
-- autogeneriert unten anderem: addr :: Person -> Address
|
|
|
|
setName :: String -> Person -> Person
|
|
setName n p = p { name = n } --record update notation
|
|
|
|
setPostcode :: String -> Person -> Person
|
|
setPostcode pc p
|
|
= p { addr = addr p { postcode = pc } }
|
|
-- update of a record inside a record
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
### Probleme
|
|
|
|
Probleme mit diesem Code:
|
|
|
|
- für 1-Dimensionale Felder ist die record-syntax ok.
|
|
- tiefere Ebenen nur umständlich zu erreichen
|
|
- eigentlich wollen wir nur pe in p setzen, müssen aber über addr etc. gehen.
|
|
- wir brauchen wissen über die "Zwischenstrukturen", an denen wir nicht
|
|
interessiert sind
|
|
|
|
### Was wir gern hätten
|
|
|
|
~~~ { .haskell }
|
|
data Person = P { name :: String
|
|
, addr :: Address
|
|
, salary :: Int }
|
|
-- a lens for each field
|
|
lname :: Lens' Person String
|
|
laddr :: Lens' Person Adress
|
|
lsalary :: Lens' Person Int
|
|
-- getter/setter for them
|
|
view :: Lens' s a -> s -> a
|
|
set :: Lens' s a -> a -> s -> s
|
|
-- lens-composition
|
|
composeL :: Lens' s1 s2 -> Lens s2 a -> Lens' s1 a
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
### Wie uns das hilft
|
|
|
|
Mit diesen Dingen (wenn wir sie hätten) könnte man dann
|
|
|
|
~~~ { .haskell }
|
|
data Person = P { name :: String
|
|
, addr :: Address
|
|
, salary :: Int }
|
|
data Address = A { road :: String
|
|
, city :: String
|
|
, postcode :: String }
|
|
setPostcode :: String -> Person -> Person
|
|
setPostcode pc p
|
|
= set (laddr `composeL` lpostcode) pc p
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
machen und wäre fertig.
|
|
|
|
## Trivialer Ansatz
|
|
|
|
### Getter/Setter als Lens-Methoden
|
|
|
|
~~~ { .haskell }
|
|
data LensR s a = L { viewR :: s -> a
|
|
, setR :: a -> s -> s }
|
|
|
|
composeL (L v1 u1) (L v2 u2)
|
|
= L (\s -> v2 (v1 s))
|
|
(\a s -> u1 (u2 a (v1 s)) s)
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
### Wieso ist das schlecht?
|
|
|
|
- extrem ineffizient
|
|
|
|
Auslesen traversiert die Datenstruktur, dann wird die Funktion angewendet und
|
|
zum setzen wird die Datenstruktur erneut traversiert:
|
|
|
|
~~~ { .haskell }
|
|
over :: LensR s a -> (a -> a) -> s -> s
|
|
over ln f s = setR l (f (viewR l s)) s
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
- Lösung: modify-funktion hinzufügen
|
|
|
|
~~~ { .haskell }
|
|
data LensR s a
|
|
= L { viewR :: s -> a
|
|
, setR :: a -> s -> s
|
|
, mod :: (a->a) -> s -> s
|
|
, modM :: (a->Maybe a) -> s -> Maybe s
|
|
, modIO :: (a->IO a) -> s -> IO s }
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Neues Problem: Für jeden Spezialfall muss die Lens erweitert werden.
|
|
|
|
### Something in common
|
|
|
|
Man kann alle Monaden abstrahieren. Functor reicht schon:
|
|
|
|
~~~ { .haskell }
|
|
data LensR s a
|
|
= L { viewR :: s -> a
|
|
, setR :: a -> s -> s
|
|
, mod :: (a->a) -> s -> s
|
|
, modF :: Functor f => (a->f a) -> s -> f s }
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Idee: Die 3 darüberliegenden durch modF ausdrücken.
|
|
|
|
### Typ einer Lens
|
|
|
|
Wenn man das berücksichtigt, dann hat einen Lens folgenden Typ:
|
|
|
|
~~~ {.haskell}
|
|
type Lens' s a = forall f. Functor f
|
|
=> (a -> f a) -> s -> f s
|
|
~~~
|
|
|
|
Allerdings haben wir dann noch unseren getter/setter:
|
|
|
|
~~~ {.haskell}
|
|
data LensR s a = L { viewR :: s -> a
|
|
, setR :: a -> s -> s }
|
|
~~~
|
|
|
|
Stellt sich raus: Die sind isomorph! Auch wenn die von den Typen her komplett
|
|
anders aussehen.
|
|
|
|
## Benutzen einer Lens als Setter
|
|
|
|
~~~ { .haskell }
|
|
set :: Lens' s a -> (a -> s -> s)
|
|
set ln a s = --...umm...
|
|
--:t ln => (a -> f a) -> s -> f s
|
|
-- => get s out of f s to return it
|
|
~~~~~~
|
|
|
|
Wir können für f einfach die "Identity"-Monade nehmen, die wir nachher wegcasten
|
|
können.
|
|
|
|
~~~ { .haskell }
|
|
newtype Identity a = Identity a
|
|
-- Id :: a -> Identity a
|
|
|
|
runIdentity :: Identity s -> s
|
|
runIdentity (Identity x) = x
|
|
|
|
instance Functor Identity where
|
|
fmap f (Identity x) = Identity (f x)
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
somit ist set einfach nur
|
|
|
|
~~~ { .haskell }
|
|
set :: Lens' s a -> (a -> s -> s)
|
|
set ln x s
|
|
= runIdentity (ls set_fld s)
|
|
where
|
|
set_fld :: a -> Identity a
|
|
set_fld _ = Identity x
|
|
-- a was the OLD value.
|
|
-- We throw that away and set the new value
|
|
~~~~~~
|
|
|
|
oder kürzer (für nerds wie den Autor der Lens-Lib)
|
|
|
|
~~~ {.haskell }
|
|
set :: Lens' s a -> (a -> s -> s)
|
|
set ln x = runIdentity . ln (Identity . const x)
|
|
~~~
|
|
|
|
## Benutzen einer Lens als Modify
|
|
|
|
Dasselbe wie Set, nur dass wir den Parameter nicht entsorgen, sondern in die
|
|
mitgelieferte Funktion stopfen.
|
|
|
|
~~~ {.haskell}
|
|
over :: Lens' s a -> (a -> a) -> s -> s
|
|
over ln f = runIdentity . ln (Identity . f)
|
|
~~~
|
|
|
|
## Benutzen einer Lens als Getter
|
|
|
|
~~~ { .haskell }
|
|
view :: Lens' s a -> (s -> a)
|
|
view ln s = --...umm...
|
|
--:t ln => (a -> f a) -> s -> f s
|
|
-- => get a out of the (f s) return-value
|
|
-- Wait, WHAT?
|
|
~~~~~~
|
|
|
|
Auch hier gibt es einen netten Funktor. Wir packen das "a" einfach in das "f"
|
|
und werfen das "s" am Ende weg.
|
|
|
|
~~~ { .haskell }
|
|
newtype Const v a = Const v
|
|
|
|
getConst :: Const v a -> v
|
|
getConst (Const x) = x
|
|
|
|
instance Functor (Const v) where
|
|
fmap f (Const x) = Const x
|
|
-- throw f away. Nothing changes our const!
|
|
~~~~~~
|
|
|
|
somit ergibt sich
|
|
|
|
~~~ { .haskell }
|
|
view :: Lens' s a -> (s -> a)
|
|
view ln s
|
|
= getConst (ln Const s)
|
|
-- Const :: s -> Const a s
|
|
~~~~~~
|
|
|
|
oder nerdig
|
|
|
|
~~~ {.haskell}
|
|
view :: Lens' s a -> (s -> a)
|
|
view ln = getConst . ln Const
|
|
~~~
|
|
|
|
## Lenses bauen
|
|
|
|
Nochmal kurz der Typ:
|
|
|
|
~~~ {.haskell}
|
|
type Lens' s a = forall f. Functor f
|
|
=> (a -> f a) -> s -> f s
|
|
~~~
|
|
|
|
Für unser Personen-Beispiel vom Anfang:
|
|
|
|
~~~ { .haskell }
|
|
data Person = P { _name :: String, _salary :: Int }
|
|
|
|
name :: Lens' Person String
|
|
-- name :: Functor f => (String -> f String)
|
|
-- -> Person -> f Person
|
|
|
|
name elt_fn (P n s)
|
|
= fmap (\n' -> P n' s) (elt_fn n)
|
|
-- fmap :: Functor f => (a->b) -> f a -> f b - der Funktor, der alles verknüpft
|
|
-- \n' -> .. :: String -> Person - Funktion um das Element zu lokalisieren (WO wird ersetzt/gelesen/...)
|
|
-- elt_fn n :: f String - Funktion um das Element zu verändern (setzen, ändern, ...)
|
|
~~~~~~
|
|
|
|
Die Lambda-Funktion ersetzt einfach den Namen. Häufig sieht man auch
|
|
|
|
~~~ {.haskell}
|
|
name elt_fn (P n s)
|
|
= (\n' -> P n' s) <$> (elt_fn n)
|
|
-- | Focus | |Function|
|
|
~~~
|
|
|
|
## Wie funktioniert das intern?
|
|
|
|
~~~ { .haskell }
|
|
view name (P {_name="Fred", _salary=100})
|
|
-- inline view-function
|
|
= getConst (name Const (P {_name="Fred", _salary=100})
|
|
-- inline name
|
|
= getConst (fmap (\n' -> P n' 100) (Const "Fred"))
|
|
-- fmap f (Const x) = Const x - Definition von Const
|
|
= getConst (Const "Fred")
|
|
-- getConst (Const x) = x
|
|
= "Fred"
|
|
~~~~~~
|
|
|
|
Dieser Aufruf hat KEINE Runtime-Kosten, weil der Compiler direkt die Adresse des
|
|
Feldes einsetzen kann. Der gesamte Boilerplate-Code wird vom Compiler
|
|
wegoptimiert.
|
|
|
|
Dies gilt für jeden Funktor mit newtype, da das nur ein Typalias ist.
|
|
|
|
## Composing Lenses und deren Benutzung
|
|
|
|
Wie sehen denn die Typen aus?
|
|
|
|
Wir wollen ein
|
|
|
|
> Lens' s1 s2 -> Lens' s2 a -> Lens' s1 a
|
|
|
|
Wir haben 2 Lenses
|
|
|
|
> ln1 :: (s2 -> f s2) -> (s1 -> f s1)
|
|
> ln2 :: (a -> f a) -> (s2 -> f s2)
|
|
|
|
wenn man scharf hinsieht, kann man die verbinden
|
|
|
|
> ln1 . ln2 :: (a -> f s) -> (s1 -> f s1)
|
|
|
|
und erhält eine Lens. Sogar die Gewünschte!
|
|
Somit ist Lens-Composition einfach nur Function-Composition (.).
|
|
|
|
## Automatisieren mit Template-Haskell
|
|
|
|
Der Code um die Lenses zu bauen ist für records immer Identisch:
|
|
|
|
~~~ { .haskell }
|
|
data Person = P { _name :: String, _salary :: Int }
|
|
|
|
name :: Lens' Person String
|
|
name elt_fn (P n s) = (\n' -> P n' s) <$> (elt_fn n)
|
|
~~~~~~
|
|
|
|
Daher kann man einfach
|
|
|
|
~~~ { .haskell }
|
|
import Control.Lens.TH
|
|
data Person = P { _name :: String, _salary :: Int }
|
|
|
|
$(makeLenses ''Person)
|
|
~~~~~~
|
|
|
|
nehmen, was einem eine Lens für "name" und eine Lens für "salary" generiert.
|
|
Mit anderen Templates kann man auch weitere Dinge steuern (etwa wofür Lenses
|
|
generiert werden, welches Prefix (statt \_) man haben will etc. pp.).
|
|
|
|
Will man das aber haben, muss man selbst in den Control.Lens.TH-Code schauen.
|
|
|
|
## Lenses für den Beispielcode
|
|
|
|
~~~ { .haskell }
|
|
import Control.Lens.TH
|
|
|
|
data Person = P { _name :: String
|
|
, _addr :: Address
|
|
, _salary :: Int }
|
|
data Address = A { _road :: String
|
|
, _city :: String
|
|
, _postcode :: String }
|
|
|
|
$(makeLenses ''Person)
|
|
$(makeLenses ''Address)
|
|
|
|
setPostcode :: String -> Person -> Person
|
|
setPostcode pc p = set (addr . postcode) pc p
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
## Shortcuts mit "Line-Noise"
|
|
|
|
~~~ { .haskell }
|
|
-- ...
|
|
|
|
setPostcode :: String -> Person -> Person
|
|
setPostcode pc p = addr . postcode .~ pc $ p
|
|
-- | Focus |set|to what|in where
|
|
|
|
getPostcode :: Person -> String
|
|
getPostcode p = p ^. $ addr . postcode
|
|
-- |from|get| Focus |
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Es gibt drölf-zillionen weitere Infix-Operatoren (für Folds,
|
|
Listenkonvertierungen, -traversierungen, ...)
|
|
|
|
## Virtuelle Felder
|
|
|
|
Man kann mit Lenses sogar Felder emulieren, die gar nicht da sind. Angenommen
|
|
folgender Code:
|
|
|
|
~~~ { .haskell }
|
|
data Temp = T { _fahrenheit :: Float }
|
|
|
|
$(makeLenses ''Temp)
|
|
-- liefert Lens: fahrenheit :: Lens Temp Float
|
|
|
|
centigrade :: Lens Temp Float
|
|
centigrade centi_fn (T faren)
|
|
= (\centi' -> T (cToF centi'))
|
|
<$> (centi_fn (fToC faren))
|
|
-- cToF & fToC as Converter-Functions defined someplace else
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Hiermit kann man dann auch Funktionen, die auf Grad-Celsius rechnen auf Daten
|
|
anwenden, die eigenlich nur Fahrenheit speichern, aber eine Umrechnung
|
|
bereitstellen. Analog kann man auch einen Zeit-Datentypen definieren, der
|
|
intern mit Sekunden rechnet (und somit garantiert frei von Fehlern wie -3
|
|
Minuten oder 37 Stunden ist)
|
|
|
|
## Non-Record Strukturen
|
|
|
|
Das ganze kann man auch parametrisieren und auf Non-Record-Strukturen anwenden.
|
|
Beispielhaft an einer Map verdeutlicht:
|
|
|
|
~~~ { .haskell }
|
|
-- from Data.Lens.At
|
|
at :: Ord k => k -> Lens' (Map k v) (Maybe v)
|
|
|
|
-- oder identisch, wenn man die Lens' auflöst:
|
|
at :: Ord k, forall f. Functor f => k -> (Maybe v -> f Maybe v) -> Map k v -> f Map k v
|
|
|
|
at k mb_fn m
|
|
= wrap <$> (mb_fn mv)
|
|
where
|
|
mv = Map.lookup k m
|
|
|
|
wrap :: Maybe v -> Map k v
|
|
wrap (Just v') = Map.insert k v' m
|
|
wrap Nothing = case mv of
|
|
Nothing -> m
|
|
Just _ -> Map.delete k m
|
|
|
|
-- mb_fn :: Maybe v -> f Maybe v
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
## Weitere Beispiele
|
|
|
|
- Bitfields auf Strukturen die Bits haben (Ints, ...) in Data.Bits.Lens
|
|
- Web-scraper in Package hexpat-lens
|
|
|
|
~~~ { .haskell }
|
|
p ^.. _HTML' . to allNodes
|
|
. traverse . named "a"
|
|
. traverse . ix "href"
|
|
. filtered isLocal
|
|
. to trimSpaces
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Zieht alle externen Links aus dem gegebenen HTML-Code in p um weitere ziele
|
|
fürs crawlen zu finden.
|
|
|
|
## Erweiterungen
|
|
|
|
Bisher hatten wir Lenses nur auf Funktoren F. Die nächstmächtigere Klasse ist
|
|
Applicative.
|
|
|
|
~~~ { .haskell }
|
|
type Traversal' s a = forall f. Applicative f
|
|
=> (a -> f a) -> (s -> f s)
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Da wir den Container identisch lassen (weder s noch a wurde angefasst) muss sich
|
|
etwas anderes ändern. Statt eines einzelnen Focus erhalten wir viele Foci.
|
|
|
|
Was ist ein Applicative überhaupt? Eine schwächere Monade (nur 1x Anwendung und
|
|
kein Bind - dafür kann man die beliebig oft hintereinanderhängen).
|
|
|
|
~~~ { .haskell }
|
|
class Functor f => Applicative f where
|
|
pure :: a -> f a
|
|
(<*>) :: f (a -> b) -> f a -> f b
|
|
|
|
-- Monade als Applicative:
|
|
pure = return
|
|
mf <*> mx = do { f <- mf; x <- mx; return (f x) }
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Recap: Was macht eine Lens:
|
|
|
|
~~~ { .haskell }
|
|
data Adress = A { _road :: String
|
|
, _city :: String
|
|
, _postcode :: String }
|
|
|
|
road :: Lens' Adress String
|
|
road elt_fn (A r c p) = (\r' -> A r' c p) <$> (elt_fn r)
|
|
-- | "Hole" | | Thing to put in|
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Wenn man nun road & city gleichzeitig bearbeiten will:
|
|
|
|
~~~ { .haskell }
|
|
addr_strs :: Traversal' Address String
|
|
addr_strs elt_fn (A r c p)
|
|
= ... (\r' c' -> A r' c' p) .. (elt_fn r) .. (elt_fn c) ..
|
|
-- | function with 2 "Holes"| first Thing | second Thing
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
fmap kann nur 1 Loch stopfen, aber nicht mit n Löchern umgehen. Applicative mit
|
|
<*> kann das.
|
|
Somit gibt sich
|
|
|
|
~~~ { .haskell }
|
|
addr_strs :: Traversal' Address String
|
|
addr_strs elt_fn (A r c p)
|
|
= pure (\r' c' -> A r' c' p) <*> (elt_fn r) <*> (elt_fn c)
|
|
-- lift in Appl. | function with 2 "Holes"| first Thing | second Thing
|
|
-- oder kürzer
|
|
addr_strs :: Traversal' Address String
|
|
addr_strs elt_fn (A r c p)
|
|
= (\r' c' -> A r' c' p) <$> (elt_fn r) <*> (elt_fn c)
|
|
-- pure x <*> y == x <$> y
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
Wie würd eine modify-funktion aussehen?
|
|
|
|
~~~ {.haskell}
|
|
over :: Lens' s a -> (a -> a) -> s -> s
|
|
over ln f = runIdentity . ln (Identity . f)
|
|
|
|
over :: Traversal' s a -> (a -> a) -> s -> s
|
|
over ln f = runIdentity . ln (Identity . f)
|
|
~~~
|
|
|
|
Der Code ist derselbe - nur der Typ ist generischer. Auch die anderen Dinge
|
|
funktioniert diese Erweiterung (für Identity und Const muss man noch ein paar
|
|
dummy-Instanzen schreiben um sie von Functor auf Applicative oder Monad zu heben
|
|
- konkret reicht hier die Instanzierung von Monoid). In der Lens-Library ist
|
|
daher meist Monad m statt Functor f gefordert.
|
|
|
|
## Wozu dienen die Erweiterungen?
|
|
|
|
Man kann mit Foci sehr selektiv vorgehen. Auch kann man diese durch Funktionen
|
|
steuern. Beispisweise eine Funktion anwenden auf
|
|
|
|
- Jedes 2. Listenelement
|
|
- Alle graden Elemente in einem Baum
|
|
- Alle Namen in einer Tabelle, deren Gehalt > 10.000€ ist
|
|
|
|
Traversals und Lenses kann man trivial kombinieren (`lens . lens` => `lens`,
|
|
`lens . traversal` => `traversal` etc.)
|
|
|
|
## Wie es in Lens wirklich aussieht
|
|
|
|
In diesem Artikel wurde nur auf Monomorphic Lenses eingegangen. In der richtigen
|
|
Library ist eine Lens
|
|
|
|
~~~ {.haskell}
|
|
type Lens' s a = Lens s s a a
|
|
type Lens s t a b = forall f. Functor f => (a -> f b) -> (s -> f t)
|
|
~~~
|
|
|
|
sodass sich auch die Typen ändern können um z.B. automatisch einen Konvertierten
|
|
(sicheren) Typen aus einer unsicheren Datenstruktur zu geben.
|
|
|
|
Die modify-Funktion over ist auch
|
|
|
|
~~~ {.haskell}
|
|
> over :: Profunctor p => Setting p s t a b -> p a b -> s -> t
|
|
~~~
|
|
|
|
> *Edward is deeply in thrall to abstractionitis* - Simon Peyton Jones
|
|
|
|
Lens alleine definiert 39 newtypes, 34 data-types und 194 Typsynonyme...
|
|
Ausschnitt
|
|
|
|
~~~ { .haskell }
|
|
-- traverseOf :: Functor f => Iso s t a b -> (a -> f b) -> s -> f t
|
|
-- traverseOf :: Functor f => Lens s t a b -> (a -> f b) -> s -> f t
|
|
-- traverseOf :: Applicative f => Traversal s t a b -> (a -> f b) -> s -> f t
|
|
|
|
traverseOf :: Over p f s t a b -> p a (f b) -> s -> f t
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
dafuq?
|