Lenses
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
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
= p { name = n } --record update notation
setName n p
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
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
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
data LensR s a = L { viewR :: s -> a
setR :: a -> s -> s }
,
L v1 u1) (L v2 u2)
composeL (= L (\s -> v2 (v1 s))
-> u1 (u2 a (v1 s)) s) (\a s
Wieso ist das schlecht?
- extrem ineffizient
Auslesen traversiert die Datenstruktur, dann wird die Function angewendet und
zum setzen wird die Datenstruktur erneut traversiert:
over :: LensR s a -> (a -> a) -> s -> s
= setR l (f (viewR l s)) s over ln f s
- Lösung: modify-funktion hinzufügen
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:
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 also Setter
set :: Lens' s a -> (a -> s -> s)
= --...umm...
set ln a s --: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.
newtype Identity a = Identity a
-- Id :: a -> Identity a
runIdentity :: Identity s -> s
Identity x) = x
runIdentity (
instance Functor Identity where
fmap f (Identity x) = Identity (f x)
somit ist set einfach nur
set :: Lens' s a -> (a -> s -> s)
set ln x s= runIdentity (ls set_fld s)
where
set_fld :: a -> Identity a
= Identity x
set_fld _ -- 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)
set :: Lens' s a -> (a -> s -> s)
= runIdentity . ln (Identity . const x) set ln x
Benutzen einer Lens also Modify
Dasselbe wie Set, nur dass wir den Parameter nicht entsorgen, sondern in die
mitgelieferte Function stopfen.
over :: Lens' s a -> (a -> a) -> s -> s
= runIdentity . ln (Identity . f) over ln f
Benutzen einer Lens also Getter
view :: Lens' s a -> (s -> a)
= --...umm...
view ln s --: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.
newtype Const v a = Const v
getConst :: Const v a -> v
Const x) = x
getConst (
instance Functor (Const v) where
fmap f (Const x) = Const x
-- throw f away. Nothing changes our const!
somit ergibt sich
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)
= getConst . ln Const view ln
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:
data Person = P { _name :: String, _salary :: Int }
name :: Lens' Person String
-- name :: Functor f => (String -> f String)
-- -> Person -> f Person
P n s)
name elt_fn (= 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
P n s)
name elt_fn (= (\n' -> P n' s) <$> (elt_fn n)
-- | Focus | |Function|
Wie funktioniert das intern?
P {_name="Fred", _salary=100})
view name (-- 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:
data Person = P { _name :: String, _salary :: Int }
name :: Lens' Person String
P n s) = (\n' -> P n' s) <$> (elt_fn n) name elt_fn (
Daher kann man einfach
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
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
= set (addr . postcode) pc p setPostcode pc p
Shortcuts mit “Line-Noise”
-- ...
setPostcode :: String -> Person -> Person
= addr . postcode .~ pc $ p
setPostcode pc p -- | Focus |set|to what|in where
getPostcode :: Person -> String
= p ^. $ addr . postcode
getPostcode p -- |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:
data Temp = T { _fahrenheit :: Float }
$(makeLenses ''Temp)
-- liefert Lens: fahrenheit :: Lens Temp Float
centigrade :: Lens Temp Float
T faren)
centigrade centi_fn (= (\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:
-- 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
= Map.lookup k m
mv
wrap :: Maybe v -> Map k v
Just v') = Map.insert k v' m
wrap (Nothing = case mv of
wrap 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
^.. _HTML' . to allNodes p . 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.
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).
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
-- Monade als Applicative:
pure = return
<*> mx = do { f <- mf; x <- mx; return (f x) } mf
Recap: Was macht eine Lens:
data Adress = A { _road :: String
_city :: String
, _postcode :: String }
,
road :: Lens' Adress String
A r c p) = (\r' -> A r' c p) <$> (elt_fn r)
road elt_fn (-- | "Hole" | | Thing to put in|
Wenn man nun road & city gleichzeitig bearbeiten will:
addr_strs :: Traversal' Address String
A r c p)
addr_strs elt_fn (= ... (\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
addr_strs :: Traversal' Address String
A r c p)
addr_strs elt_fn (= 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
A r c p)
addr_strs elt_fn (= (\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
= runIdentity . ln (Identity . f)
over ln f
over :: Traversal' s a -> (a -> a) -> s -> s
= runIdentity . ln (Identity . f) over ln 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
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
-- 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?