initial
This commit is contained in:
580
Coding/Haskell/Lenses.md
Normal file
580
Coding/Haskell/Lenses.md
Normal file
@ -0,0 +1,580 @@
|
||||
---
|
||||
tags:
|
||||
- Haskell
|
||||
title: Lenses
|
||||
categories:
|
||||
- Article
|
||||
- Haskell
|
||||
date: "2018-01-01"
|
||||
---
|
||||
|
||||
## Wofür brauchen wir das überhaupt?
|
||||
|
||||
Die Idee dahinter ist, dass man Zugriffsabstraktionen über Daten verknüpfen
|
||||
kann. Also 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
|
||||
```
|
||||
|
||||
### Problem
|
||||
|
||||
Problem 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 also 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 Function 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 also 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 Author der Lens-Lib)
|
||||
|
||||
```{.haskell }
|
||||
set :: Lens' s a -> (a -> s -> s)
|
||||
set ln x = runIdentity . ln (Identity . const x)
|
||||
```
|
||||
|
||||
## Benutzen einer Lens also Modify
|
||||
|
||||
Dasselbe wie Set, nur dass wir den Parameter nicht entsorgen, sondern in die
|
||||
mitgelieferte Function stopfen.
|
||||
|
||||
```{.haskell}
|
||||
over :: Lens' s a -> (a -> a) -> s -> s
|
||||
over ln f = runIdentity . ln (Identity . f)
|
||||
```
|
||||
|
||||
## Benutzen einer Lens also 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 End 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 Address
|
||||
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 Function 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?
|
Reference in New Issue
Block a user