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

  1. Extrahera a genom att glömma bort det extra sammanhanget eller
  2. 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.

ange bildbeskrivning här

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' eller Simple 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 ur s
  • set :: Lens' sa -> (a -> s -> s) "sätter" a spåret i s
  • review :: Prism' sa -> (a -> s) "förverkligar" som en a kan vara en s
  • preview :: Prism' sa -> (s -> Maybe a) "försöker" att förvandla en s till en a .

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' bcf . 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ör Lens 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.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow