emanote/content/Haskell/Lenses.md

511 lines
15 KiB
Markdown

# Wozu 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 .numberLines }
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 .numberLines }
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 .numberLines }
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 .numberLines }
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 .numberLines }
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 .numberLines }
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 .numberLines }
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:
> type Lens' s a = forall f. Functor f
> => (a -> f a) -> s -> f s
Allerdings haben wir dann noch unseren getter/setter:
> 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 .numberLines }
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 .numberLines }
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 .numberLines }
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)
> 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.
> over :: Lens' s a -> (a -> a) -> s -> s
> over ln f = runIdentity . ln (Identity . f)
# Benutzen einer Lens als Getter
~~~ { .haskell .numberLines }
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 .numberLines }
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 .numberLines }
view :: Lens' s a -> (s -> a)
view ln s
= getConst (ln Const s)
-- Const :: s -> Const a s
~~~~~~
oder nerdig
> view :: Lens' s a -> (s -> a)
> view ln = getConst . ln Const
# Lenses bauen
Nochmal kurz der Typ:
> type Lens' s a = forall f. Functor f
> => (a -> f a) -> s -> f s
Für unser Personen-Beispiel vom Anfang:
~~~ { .haskell .numberLines }
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
> name elt_fn (P n s)
> = (\n' -> P n' s) <$> (elt_fn n)
> -- | Focus | |Function|
# Wie funktioniert das intern?
~~~ { .haskell .numberLines }
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 .numberLines }
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 .numberLines }
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 .numberLines }
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 .numberLines }
-- ...
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 .numberLines }
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 .numberLines }
-- 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 .numberLines }
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 .numberLines }
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 .numberLines }
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 .numberLines }
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 .numberLines }
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 .numberLines }
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?
> 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
> 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
> 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 .numberLines }
-- 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?