Haskell Language
Lins
Sök…
Introduktion
Lens är ett bibliotek för Haskell som tillhandahåller linser, isomorfismer, veck, traversals, getters och seters, som avslöjar ett enhetligt gränssnitt för att fråga och manipulera godtyckliga strukturer, inte till skillnad från Java: s accessor- och mutatorkoncept.
Anmärkningar
Vad är en lins?
Linser (och annan optik) tillåter oss att separera beskriva hur vi vill få åtkomst till vissa data från vad vi vill göra med det. Det är viktigt att skilja mellan det abstrakta begreppet lins och det konkreta genomförandet. Att förstå abstrakt gör programmering med lens
mycket lättare på lång sikt. Det finns många isomorfa framställningar av linser, så för denna diskussion kommer vi att undvika någon konkret implementeringsdiskussion och istället ge en hög nivå överblick över koncepten.
Fokusering
Ett viktigt begrepp för att abstrakt förstå är begreppet fokusering . Viktig optik fokuserar på en specifik del av en större datastruktur utan att glömma bort det större sammanhanget. Till exempel fokuserar linsen _1
på det första elementet i en tupel men glömmer inte vad som fanns i det andra fältet.
När vi har fokus kan vi sedan prata om vilka operationer vi får utföra med en lins. Med tanke på en Lens sa
som när vi får en datatyp av typen s
fokuserar på en specifik a
, kan vi antingen
- Extrahera
a
genom att glömma bort det extra sammanhanget eller - Byt ut
a
genom att tillhandahålla ett nytt värde
Dessa motsvarar de välkända get
och set
operationerna som vanligtvis används för att karakterisera en lins.
Annan optik
Vi kan prata om andra optiker på liknande sätt.
Optisk | Fokuserar på... |
---|---|
Lins | En del av en produkt |
Prisma | En del av summan |
Traversal | Noll eller fler delar av en datastruktur |
Isomorfi | ... |
Varje optik fokuserar på ett annat sätt, som sådan, beroende på vilken typ av optik vi har kan vi utföra olika operationer.
Sammansättning
Dessutom kan vi komponera vilken som helst av de två optiker som vi hittills har diskuterat för att specificera komplexa datatillgångar. De fyra typerna av optik som vi har diskuterat bildar ett galler, resultatet av att sammansätta två optiker tillsammans är deras övre gräns.
Om vi till exempel komponerar en lins och ett prisma får vi en genomgång. Anledningen till detta är att genom deras (vertikala) komposition fokuserar vi först på en del av en produkt och sedan på en del av en summa. Resultatet är en optik som fokuserar på exakt noll eller en del av våra data som är ett speciellt fall av en genomgång. (Detta kallas också ibland en affineraversion).
I Haskell
Anledningen till populariteten i Haskell är att det finns en mycket kortfattad representation av optiken. All optik är bara funktioner i en viss form som kan komponeras tillsammans med funktionskomposition. Detta leder till en mycket lätt inbäddning som gör det enkelt att integrera optik i dina program. På grund av informationen om kodningen beräknar funktionskompositionen också automatiskt den övre gränsen för två optiker som vi komponerar. Detta innebär att vi kan återanvända samma kombinatorer för olika optik utan uttrycklig gjutning.
Hantera tupler med lins
Komma
("a", 1) ^. _1 -- returns "a"
("a", 1) ^. _2 -- returns 1
Miljö
("a", 1) & _1 .~ "b" -- returns ("b", 1)
Ändra
("a", 1) & _2 %~ (+1) -- returns ("a", 2)
both
Traversal
(1, 2) & both *~ 2 -- returns (2, 4)
Linser för poster
Enkel skiva
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Point = Point {
_x :: Float,
_y :: Float
}
makeLenses ''Point
Linser x
och y
skapas.
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 }
Hantera poster med upprepade fältnamn
data Person = Person { _personName :: String }
makeFields ''Person
Skapar en klasstyp HasName
, lins name
för Person
och gör Person
en instans av HasName
. Efterföljande poster kommer också att läggas till klassen:
data Entity = Entity { _entityName :: String }
makeFields ''Entity
Template Haskell-förlängningen krävs för att makeFields
ska fungera. Tekniskt sett är det fullt möjligt att skapa linser gjorda på detta sätt på andra sätt, t.ex. för hand.
Statliga linser
Linsoperatörer har användbara varianter som fungerar i tillräckliga sammanhang. De erhålls genom att ersätta ~
med =
i operatörens namn.
(+~) :: Num a => ASetter s t a a -> a -> s -> t
(+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()
Obs: De tillståndsvarianterna förväntas inte ändra typen, så de har signaturerna
Lens'
ellerSimple Lens'
.
Bli av med &
kedjor
Om linsfulla operationer måste kedjas ser det ofta ut så här:
change :: A -> A
change a = a & lensA %~ operationA
& lensB %~ operationB
& lensC %~ operationC
Detta fungerar tack vare associativiteten hos &
. Den tillåtna versionen är dock tydligare.
change a = flip execState a $ do
lensA %= operationA
lensB %= operationB
lensC %= operationC
Om lensX
faktiskt är id
, kan naturligtvis hela operationen utföras direkt genom att bara lyfta den med modify
.
Imperativ kod med strukturerat tillstånd
Förutsatt att detta exempel anges:
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
Vi kan skriva kod som liknar klassiska imperativspråk samtidigt som vi fortfarande tillåter oss att använda fördelarna med Haskell:
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) %= ...
Att skriva en lins utan mall Haskell
För att avmystifiera mall Haskell, antar att du har
data Example a = Example { _foo :: Int, _bar :: a }
sedan
makeLenses 'Example
producerar (mer eller mindre)
foo :: Lens' (Example a) Int bar :: Lens (Example a) (Example b) a b
Men det finns inget särskilt magiskt på gång. Du kan skriva dessa själv:
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)
I huvudsak vill du "besöka" ditt objektiv "" fokus "med wrap
funktionen och sedan bygga om" hela "typen.
Lins och prisma
A Lens' sa
betyder att du alltid kan hitta ett a
inom vilken som helst s
. En Prism' sa
innebär att du ibland kan hitta som s
faktiskt bara är a
, men ibland är det något annat.
För att vara mer tydlig har vi _1 :: Lens' (a, b) a
eftersom varje tupel alltid har ett första element. Vi har _Just :: Prism' (Maybe a) a
för ibland Maybe a
är a faktiskt a
värde som är inslaget i Just
men ibland är det Nothing
.
Med denna intuition kan vissa standardkombinationer tolkas parallellt med varandra
-
view :: Lens' sa -> (s -> a)
"får"a
ut urs
-
set :: Lens' sa -> (a -> s -> s)
"sätter"a
spåret is
-
review :: Prism' sa -> (a -> s)
"förverkligar" som ena
kan vara ens
-
preview :: Prism' sa -> (s -> Maybe a)
"försöker" att förvandla ens
till ena
.
Ett annat sätt att tänka på det är att ett värde av typen Lens' sa
visar att s
har samma struktur som (r, a)
för några okända r
. Å andra sidan visar Prism' sa
att s
har samma struktur som Either ra
för vissa r
. Vi kan skriva de fyra funktionerna ovan med denna kunskap:
-- `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
traverse
En Traversal' sa
visar att s
har 0-till-många a
s inne i den.
toListOf :: Traversal' s a -> (s -> [a])
Någon typ t
som är Traversable
automatiskt har som traverse :: Traversal (ta) a
.
Vi kan använda en Traversal
att ställa in eller kartlägga över alla dessa a
-värden
> 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]
A f :: Lens' sa
säger att det exakt finns a
insida av s
. En g :: Prism' ab
säger att det finns antingen 0 eller 1 b
s i a
. Komposition f . g
ger oss en Traversal' sb
eftersom följande f
och sedan g
visar hur det finns 0 till 1 b
s i s
.
Linser komponerar
Om du har en f :: Lens' ab
och en g :: Lens' bc
så f . g
är en Lens' ac
acten genom att följa f
först och sedan g
. I synnerhet:
- Linser komponera som funktioner (egentligen de bara är funktioner)
- Om du tänker på
view
förLens
verkar det som att dataflöden "från vänster till höger" - detta kan känna bakåt till din normala intuition för funktionskomposition. Å andra sidan borde det känna sig naturligt om du tänker på.
-anteckning som hur det händer på OO-språk.
Mer än bara komponera Lens
med Lens
, (.)
Kan användas för att komponera nästan alla " Lens
-liknande" skriver tillsammans. Det är inte alltid lätt att se vad resultatet är eftersom typen blir tuffare att följa, men du kan använda lens
att räkna ut det. Kompositionen x . y
har typen av den minst övre gränsen av typerna av både x
och y
i diagrammet.
Klassiska linser
Control.Lens.TH
erbjuder, förutom standardfunktionen makeLenses
för generering av Lens
, även makeClassy
funktionen. makeClassy
har samma typ och fungerar i huvudsak på samma sätt som makeLenses
, med en viktig skillnad. Förutom att generera standardlinser och genomgång, om typen inte har några argument, kommer den att skapa en klass som beskriver alla datatyper som besitter typen som fält. Till exempel
data Foo = Foo { _fooX, _fooY :: Int }
makeClassy ''Foo
kommer skapa
class HasFoo t where
foo :: Simple Lens t Foo
instance HasFoo Foo where foo = id
fooX, fooY :: HasFoo t => Simple Lens t Int
Fält med makeFields
(Detta exempel kopierades från detta StackOverflow-svar )
Låt oss säga att du har ett antal olika datatyper som alla borde ha en lins med samma namn, i detta fall capacity
. makeFields
skapar en klass som klarar detta utan namnsytekonflikter.
{-# 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)
Sedan i 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
Så det som faktiskt görs förklaras som en klass HasCapacity sa
, där kapaciteten är ett Lens'
från s
till a
( a
fixas när s är känt). Den räknade ut namnet "kapacitet" genom att avlägsna det (lägre) namnet på datatypen från fältet; Jag tycker att det är trevligt att inte behöva använda en understruk på varken fältnamnet eller linsnamnet, eftersom ibland är inspelningssyntax det du vill ha. Du kan använda makeFieldsWith och de olika linsreglerna för att ha olika alternativ för att beräkna linsnamnen.
Om det hjälper, använder du ghci -ddump-skarvar 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.
Så den första splitsen gjorde klassen HasCapcity
och lade till en instans för Foo; den andra använde den befintliga klassen och gjorde en instans för Bar.
Detta fungerar också om du importerar HasCapcity
klassen från en annan modul; makeFields
kan lägga till fler instanser i den befintliga klassen och sprida dina typer ut över flera moduler. Men om du använder den igen i en annan modul där du inte har importerat klassen kommer den att skapa en ny klass (med samma namn), och du har två separata linser med överbelastad kapacitet som inte är kompatibla.