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

  1. Pak de a door de aanvullende context te vergeten of
  2. 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.

voer hier de afbeeldingsbeschrijving in

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 de Simple 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" de a uit de s
  • set :: Lens' sa -> (a -> s -> s) "stelt" de a sleuf in s
  • review :: Prism' sa -> (a -> s) "realiseert" dat een a een s zou kunnen zijn
  • preview :: Prism' sa -> (s -> Maybe a) "probeert" van een s een a .

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 van Lens , 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.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow