Haskell Language
Lens
Zoeken…
Invoering
Lens is een bibliotheek voor Haskell die lenzen, isomorfismen, vouwen, traversals, getters en setters biedt, die een uniforme interface blootlegt voor het bevragen en manipuleren van willekeurige structuren, niet in tegenstelling tot Java's accessor- en mutatorconcepten.
Opmerkingen
Wat is een lens?
Lenzen (en andere optica) stellen ons in staat om te beschrijven hoe we toegang willen krijgen tot bepaalde gegevens en wat we ermee willen doen. Het is belangrijk om onderscheid te maken tussen de abstracte notie van een lens en de concrete implementatie. Abstract begrip maakt programmeren met lens
op de lange termijn veel eenvoudiger. Er zijn veel isomorfe weergaven van lenzen, dus voor deze discussie vermijden we elke concrete implementatiediscussie en geven we in plaats daarvan een overzicht op hoog niveau van de concepten.
Scherpstellen
Een belangrijk concept om abstract te begrijpen is het begrip focussen . Belangrijke optieken richten zich op een specifiek deel van een grotere datastructuur zonder de grotere context te vergeten. De lens _1
stelt bijvoorbeeld _1
op het eerste element van een tuple, maar vergeet niet wat zich in het tweede veld bevond.
Zodra we de focus hebben, kunnen we vervolgens praten over welke bewerkingen we met een lens mogen uitvoeren. Gegeven een Lens sa
die, wanneer een gegevenstype van het type s
op een specifieke a
, kunnen we dat ook doen
- Pak de
a
door de aanvullende context te vergeten of - Vervang de
a
door een nieuwe waarde op te geven
Deze komen overeen met de bekende get
en set
bewerkingen die meestal worden gebruikt om een lens te karakteriseren.
Andere optica
We kunnen op dezelfde manier over andere optica praten.
optic | Focussen op... |
---|---|
Lens | Een deel van een product |
Prisma | Een deel van een som |
traversal | Nul of meer delen van een gegevensstructuur |
isomorfisme | ... |
Elke optiek stelt zich op een andere manier scherp, afhankelijk van het type optiek dat we hebben, kunnen we verschillende bewerkingen uitvoeren.
Samenstelling
Bovendien kunnen we elk van de twee optieken die we tot nu toe hebben besproken, samenstellen om complexe gegevenstoegangen te specificeren. De vier soorten optica die we hebben besproken vormen een rooster, het resultaat van het samenstellen van twee optica is hun bovengrens.
Als we bijvoorbeeld een lens en een prisma samenstellen, krijgen we een doortocht. De reden hiervoor is dat we ons door hun (verticale) samenstelling eerst concentreren op één deel van een product en vervolgens op één deel van een som. Het resultaat is een optiek die zich richt op precies nul of één deel van onze gegevens, wat een speciaal geval is van een doorgang. (Dit wordt ook wel een affiene transversatie genoemd).
In Haskell
De reden voor de populariteit in Haskell is dat er een zeer beknopte weergave van optica is. Alle optica zijn slechts functies van een bepaalde vorm die samen kunnen worden samengesteld met behulp van functiesamenstelling. Dit leidt tot een zeer lichte inbedding waardoor het eenvoudig is om optica in uw programma's te integreren. Bovendien berekent functiesamenstelling door de bijzonderheden van de codering ook automatisch de bovengrens van twee optica die we samenstellen. Dit betekent dat we dezelfde combinators voor verschillende optica kunnen hergebruiken zonder expliciete casting.
Tupels manipuleren met lens
Getting
("a", 1) ^. _1 -- returns "a"
("a", 1) ^. _2 -- returns 1
omgeving
("a", 1) & _1 .~ "b" -- returns ("b", 1)
wijzigen
("a", 1) & _2 %~ (+1) -- returns ("a", 2)
both
Traversal
(1, 2) & both *~ 2 -- returns (2, 4)
Lenzen voor records
Eenvoudig opnemen
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Point = Point {
_x :: Float,
_y :: Float
}
makeLenses ''Point
Lenzen x
en y
worden gemaakt.
let p = Point 5.0 6.0
p ^. x -- returns 5.0
set x 10 p -- returns Point { _x = 10.0, _y = 6.0 }
p & x +~ 1 -- returns Point { _x = 6.0, _y = 6.0 }
Records beheren met herhalende veldnamen
data Person = Person { _personName :: String }
makeFields ''Person
Maakt een soort klasse HasName
, lens name
voor de Person
, en maakt Person
een instantie van HasName
. Volgende records worden ook aan de klas toegevoegd:
data Entity = Entity { _entityName :: String }
makeFields ''Entity
De extensie Template Haskell is vereist om makeFields
te laten werken. Technisch gezien is het heel goed mogelijk om de lenzen die op deze manier zijn gemaakt op andere manieren te maken, bijvoorbeeld met de hand.
Stateful Lenzen
Lensoperators hebben nuttige varianten die in stateful contexten werken. Ze worden verkregen door ~
vervangen door =
in de naam van de operator.
(+~) :: Num a => ASetter s t a a -> a -> s -> t
(+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()
Opmerking: Van de stateful-varianten wordt niet verwacht dat ze het type veranderen, dus ze hebben de handtekeningen van de
Lens'
of deSimple Lens'
.
Het wegwerken van &
ketens
Als lens-ful-operaties moeten worden gekoppeld, ziet dit er vaak zo uit:
change :: A -> A
change a = a & lensA %~ operationA
& lensB %~ operationB
& lensC %~ operationC
Dit werkt dankzij de associativiteit van &
. De stateful-versie is echter duidelijker.
change a = flip execState a $ do
lensA %= operationA
lensB %= operationB
lensC %= operationC
Als lensX
daadwerkelijk id
, kan de hele bewerking natuurlijk direct worden uitgevoerd door deze gewoon op te heffen met modify
.
Gebiedende wijs met gestructureerde staat
Uitgaande van dit voorbeeld staat:
data Point = Point { _x :: Float, _y :: Float }
data Entity = Entity { _position :: Point, _direction :: Float }
data World = World { _entities :: [Entity] }
makeLenses ''Point
makeLenses ''Entity
makeLenses ''World
We kunnen code schrijven die lijkt op klassieke imperatieve talen, terwijl we toch de voordelen van Haskell kunnen gebruiken:
updateWorld :: MonadState World m => m ()
updateWorld = do
-- move the first entity
entities . ix 0 . position . x += 1
-- do some operation on all of them
entities . traversed . position %= \p -> p `pointAdd` ...
-- or only on a subset
entities . traversed . filtered (\e -> e ^. position.x > 100) %= ...
Een lens schrijven zonder sjabloon Haskell
Om Template Haskell te demystificeren, stel dat u dat hebt gedaan
data Example a = Example { _foo :: Int, _bar :: a }
vervolgens
makeLenses 'Example
produceert (min of meer)
foo :: Lens' (Example a) Int bar :: Lens (Example a) (Example b) a b
Er is echter niets bijzonders aan de hand. Je kunt deze zelf schrijven:
foo :: Lens' (Example a) Int -- :: Functor f => (Int -> f Int) -> (Example a -> f (Example a)) ;; expand the alias foo wrap (Example foo bar) = fmap (\newFoo -> Example newFoo bar) (wrap foo) bar :: Lens (Example a) (Example b) a b -- :: Functor f => (a -> f b) -> (Example a -> f (Example b)) ;; expand the alias bar wrap (Example foo bar) = fmap (\newBar -> Example foo newBar) (wrap bar)
In wezen wilt u de "focus" van uw lens "bezoeken" met de wrap
functie en vervolgens het "hele" type opnieuw opbouwen.
Lens en prisma
Een Lens' sa
betekent dat u altijd een a
kunt vinden binnen elke s
. Een Prism' sa
betekent dat je soms vinden dat s
eigenlijk gewoon a
, maar soms is het iets anders.
Voor de duidelijkheid hebben we _1 :: Lens' (a, b) a
omdat elke tuple altijd een eerste element heeft. We hebben _Just :: Prism' (Maybe a) a
want soms Maybe a
is eigenlijk een a
waarde verpakt in Just
maar soms is het Nothing
.
Met deze intuïtie kunnen sommige standaardcombinators parallel aan elkaar worden geïnterpreteerd
-
view :: Lens' sa -> (s -> a)
"haalt" dea
uit des
-
set :: Lens' sa -> (a -> s -> s)
"stelt" dea
sleuf ins
-
review :: Prism' sa -> (a -> s)
"realiseert" dat eena
eens
zou kunnen zijn -
preview :: Prism' sa -> (s -> Maybe a)
"probeert" van eens
eena
.
Een andere manier om erover na te denken is dat een waarde van het type Lens' sa
aantoont dat s
dezelfde structuur heeft als (r, a)
voor een onbekende r
. Aan de andere kant, Prism' sa
s
laat zien dat s
dezelfde structuur heeft als Either ra
voor sommige r
. We kunnen die vier functies hierboven schrijven met deze kennis:
-- `Lens' s a` is no longer supplied, instead we just *know* that `s ~ (r, a)` view :: (r, a) -> a view (r, a) = a set :: a -> (r, a) -> (r, a) set a (r, _) = (r, a) -- `Prism' s a` is no longer supplied, instead we just *know* that `s ~ Either r a` review :: a -> Either r a review a = Right a preview :: Either r a -> Maybe a preview (Left _) = Nothing preview (Right a) = Just a
Traversals
Een Traversal' sa
toont dat s
heeft 0 to many a
s erin.
toListOf :: Traversal' s a -> (s -> [a])
Elk type t
dat Traversable
, heeft automatisch die traverse :: Traversal (ta) a
.
We kunnen gebruik maken van een Traversal
instellen of kaart over al deze a
waarden
> set traverse 1 [1..10]
[1,1,1,1,1,1,1,1,1,1]
> over traverse (+1) [1..10]
[2,3,4,5,6,7,8,9,10,11]
Een f :: Lens' sa
zegt dat er precies één a
binnenkant van s
. A g :: Prism' ab
zegt dat er 0 of 1 b
s in a
. Componeren f . g
geeft ons een Traversal' sb
omdat het volgen van f
en dan g
laat zien hoe er 0-naar-1 b
's in s
.
Lenzen samenstellen
Als je een f :: Lens' ab
en een g :: Lens' bc
dan f . g
is een Lens' ac
verkregen door eerst f
en dan g
. Met name:
- Lenzen componeren als functies (eigenlijk ze gewoon functies)
- Als u denkt aan de
view
vanLens
, lijkt het erop dat gegevensstromen "van links naar rechts" stromen - dit kan terugvallen op uw normale intuïtie voor functiesamenstelling. Aan de andere kant zou het natuurlijk moeten aanvoelen als je eraan denkt.
-notatie zoals hoe het gebeurt in OO-talen.
Meer dan alleen het samenstellen van Lens
met Lens
, (.)
Kan worden gebruikt voor het samenstellen bijna elke " Lens
-achtige" bij elkaar te typen. Het is niet altijd gemakkelijk om te zien wat het resultaat is, omdat het type moeilijker te volgen wordt, maar u kunt de lens
om erachter te komen. De samenstelling x . y
heeft het type van de minst bovengrens van de typen van zowel x
als y
in die grafiek.
Stijlvolle lenzen
In aanvulling op de standaard makeLenses
functioneren voor het opwekken Lens
es, Control.Lens.TH
biedt ook het makeClassy
functie. makeClassy
heeft hetzelfde type en werkt in wezen op dezelfde manier als makeLenses
, met één belangrijk verschil. Naast het genereren van de standaardlenzen en -doorkruisen, als het type geen argumenten heeft, zal het ook een klasse creëren die alle datatypes beschrijft die het type als een veld bezitten. Bijvoorbeeld
data Foo = Foo { _fooX, _fooY :: Int }
makeClassy ''Foo
zal maken
class HasFoo t where
foo :: Simple Lens t Foo
instance HasFoo Foo where foo = id
fooX, fooY :: HasFoo t => Simple Lens t Int
Velden met makeFields
(Dit voorbeeld is gekopieerd van dit StackOverflow-antwoord )
Stel dat u een aantal verschillende gegevenstypen heeft die allemaal een lens met dezelfde naam moeten hebben, in dit geval capacity
. Het segment makeFields
maakt een klasse die dit doet zonder naamruimteconflicten.
{-# LANGUAGE FunctionalDependencies
, MultiParamTypeClasses
, TemplateHaskell
#-}
module Foo
where
import Control.Lens
data Foo
= Foo { fooCapacity :: Int }
deriving (Eq, Show)
$(makeFields ''Foo)
data Bar
= Bar { barCapacity :: Double }
deriving (Eq, Show)
$(makeFields ''Bar)
Vervolgens in ghci:
*Foo
λ let f = Foo 3
| b = Bar 7
|
b :: Bar
f :: Foo
*Foo
λ fooCapacity f
3
it :: Int
*Foo
λ barCapacity b
7.0
it :: Double
*Foo
λ f ^. capacity
3
it :: Int
*Foo
λ b ^. capacity
7.0
it :: Double
λ :info HasCapacity
class HasCapacity s a | s -> a where
capacity :: Lens' s a
-- Defined at Foo.hs:14:3
instance HasCapacity Foo Int -- Defined at Foo.hs:14:3
instance HasCapacity Bar Double -- Defined at Foo.hs:19:3
Dus wat het eigenlijk is gedaan, wordt uitgeroepen tot een klasse HasCapacity sa
, waarbij de capaciteit een Lens'
van s
tot a
( a
wordt vastgesteld zodra s bekend is). Het heeft de naam "capaciteit" gevonden door de (lagere) naam van het gegevenstype uit het veld te verwijderen; Ik vind het prettig om geen onderstrepingsteken te hoeven gebruiken op de veldnaam of de lensnaam, omdat soms de syntaxis van de opname eigenlijk is wat je wilt. U kunt makeFieldsWith en de verschillende lensRules gebruiken om een aantal verschillende opties te hebben voor het berekenen van de lensnamen.
In het geval dat het helpt, met behulp van ghci -ddump-splices Foo.hs:
[1 of 1] Compiling Foo ( Foo.hs, interpreted )
Foo.hs:14:3-18: Splicing declarations
makeFields ''Foo
======>
class HasCapacity s a | s -> a where
capacity :: Lens' s a
instance HasCapacity Foo Int where
{-# INLINE capacity #-}
capacity = iso (\ (Foo x_a7fG) -> x_a7fG) Foo
Foo.hs:19:3-18: Splicing declarations
makeFields ''Bar
======>
instance HasCapacity Bar Double where
{-# INLINE capacity #-}
capacity = iso (\ (Bar x_a7ne) -> x_a7ne) Bar
Ok, modules loaded: Foo.
Dus de eerste splitsing maakte de klasse HasCapcity
en voegde een instantie voor Foo toe; de tweede gebruikte de bestaande klasse en maakte een instantie voor Bar.
Dit werkt ook als u de HasCapcity
klasse vanuit een andere module importeert; makeFields
kan meer instanties aan de bestaande klasse toevoegen en uw typen over meerdere modules spreiden. Maar als je het opnieuw gebruikt in een andere module waar je de klasse niet hebt geïmporteerd, maakt het een nieuwe klasse (met dezelfde naam) en heb je twee afzonderlijke lenzen met overbelaste capaciteit die niet compatibel zijn.