# 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?