emanote/content/Coding/Haskell/Lenses.md

15 KiB

Lenses

Wofür 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?