15 KiB
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
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
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 als Lens-Methoden
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:
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
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 als Setter
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.
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
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
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.
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
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:
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?
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:
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
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
setPostcode pc p = set (addr . postcode) pc p
Shortcuts mit "Line-Noise"
-- ...
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:
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:
-- 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
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.
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
mf <*> mx = do { f <- mf; x <- mx; return (f x) }
Recap: Was macht eine Lens:
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:
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
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
-- 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?