Ricerca…


introduzione

Lens è una libreria per Haskell che fornisce obiettivi, isomorfismi, pieghe, attraversamenti, getter e setter, che espone un'interfaccia uniforme per interrogare e manipolare strutture arbitrarie, non diversamente dai concetti di accesso e mutatore di Java.

Osservazioni

Cos'è un obiettivo?

Gli obiettivi (e altre ottiche) ci permettono di separare la descrizione di come vogliamo accedere ad alcuni dati da ciò che vogliamo fare con esso. È importante distinguere tra la nozione astratta di lente e l'implementazione concreta. Comprendere astrattamente rende la programmazione con l' lens molto più facile a lungo termine. Ci sono molte rappresentazioni isomorfe degli obiettivi, quindi per questa discussione eviteremo ogni discussione concreta di implementazione e daremo invece una panoramica di alto livello dei concetti.

Messa a fuoco

Un concetto importante nella comprensione astratta è la nozione di messa a fuoco . L'ottica importante si concentra su una parte specifica di una struttura dati più ampia senza dimenticare il contesto più ampio. Ad esempio, l'obiettivo _1 concentra sul primo elemento di una tupla ma non dimentica ciò che era nel secondo campo.

Una volta focalizzati, possiamo quindi parlare delle operazioni che siamo autorizzati a eseguire con una lente. Data una Lens sa che una volta dato un tipo di dati di tipo s si concentra su uno specifico a , possiamo sia

  1. Estrarre la a dimenticando il contesto aggiuntivo o
  2. Sostituisci la a fornendo un nuovo valore

Questi corrispondono alle ben note operazioni get e set che vengono solitamente utilizzate per caratterizzare un obiettivo.

Altre ottiche

Possiamo parlare di altre ottiche in modo simile.

ottico Si concentra su...
lente Una parte di un prodotto
Prisma Una parte di una somma
Traversal Zero o più parti di una struttura di dati
Isomorfismo ...

Ogni ottica si focalizza in un modo diverso, in quanto tale, a seconda del tipo di ottica che abbiamo, possiamo eseguire diverse operazioni.

Composizione

Inoltre, possiamo comporre qualsiasi delle due ottiche che abbiamo discusso finora per specificare accessi dati complessi. I quattro tipi di ottica che abbiamo discusso formano un reticolo, il risultato della composizione di due ottiche insieme è il loro limite superiore.

inserisci la descrizione dell'immagine qui

Ad esempio, se componiamo insieme una lente e un prisma, otteniamo un attraversamento. La ragione di ciò è che con la loro composizione (verticale), ci concentriamo prima su una parte di un prodotto e poi su una parte di una somma. Il risultato è un'ottica che si concentra precisamente su zero o su una parte dei nostri dati, che è un caso particolare di attraversamento. (Questo è anche a volte chiamato un traversal affine).

In Haskell

La ragione della popolarità in Haskell è che esiste una rappresentazione dell'ottica molto succinta. Tutte le ottiche sono solo funzioni di una certa forma che possono essere composte insieme usando la composizione della funzione. Questo porta ad un incorporamento molto leggero che rende facile integrare l'ottica nei tuoi programmi. Inoltre, a causa delle particolarità della codifica, la composizione della funzione calcola automaticamente anche il limite superiore di due ottiche che componiamo. Ciò significa che possiamo riutilizzare gli stessi combinatori per diverse ottiche senza casting esplicito.

Manipolare le tuple con la lente

ottenere

("a", 1) ^. _1 -- returns "a"
("a", 1) ^. _2 -- returns 1

Ambientazione

("a", 1) & _1 .~ "b" -- returns ("b", 1)

Modifica

("a", 1) & _2 %~ (+1) -- returns ("a", 2)

both Traversal

(1, 2) & both *~ 2 -- returns (2, 4)

Lenti per dischi

Registrazione semplice

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

data Point = Point {
    _x :: Float,
    _y :: Float
}
makeLenses ''Point

Gli obiettivi x e y sono creati.

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 }

Gestione dei record con nomi di campi ripetuti

data Person = Person { _personName :: String }
makeFields ''Person

Crea una classe di tipo HasName , name dell'obiettivo per Person e rende Person un'istanza di HasName . I record successivi verranno aggiunti anche alla classe:

data Entity = Entity { _entityName :: String }
makeFields ''Entity

L'estensione Template Haskell è necessaria per il makeFields per funzionare. Tecnicamente, è possibile creare le lenti in questo modo con altri mezzi, ad esempio a mano.

Lenti di stato

Gli operatori di lenti hanno varianti utili che operano in contesti di stato. Si ottengono sostituendo ~ con = nel nome dell'operatore.

(+~) :: Num a => ASetter s t a a -> a -> s -> t
(+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()

Nota: non ci si aspetta che le varianti stateful cambino il tipo, quindi hanno le firme Lens' o della Simple Lens' .

Liberarsi di & catene

Se le operazioni lente devono essere concatenate, spesso assomiglia a questo:

change :: A -> A
change a = a & lensA %~ operationA
             & lensB %~ operationB
             & lensC %~ operationC

Funziona grazie all'associatività di & . La versione stateful è più chiara, però.

change a = flip execState a $ do
    lensA %= operationA
    lensB %= operationB
    lensC %= operationC

Se lensX è effettivamente id , l'intera operazione può ovviamente essere eseguita direttamente semplicemente sollevandolo con modify .

Codice imperativo con stato strutturato

Assumendo questo stato di esempio:

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

Possiamo scrivere un codice che assomiglia a linguaggi imperativi classici, pur continuando a consentirci di utilizzare i vantaggi di 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) %= ...

Scrivere una lente senza Template Haskell

Per demistificare Template Haskell, supponiamo di averlo

data Example a = Example { _foo :: Int, _bar :: a }

poi

makeLenses 'Example

produce (più o meno)

foo :: Lens' (Example a) Int
bar :: Lens (Example a) (Example b) a b

Non c'è niente di particolarmente magico in corso, però. Puoi scrivere tu stesso:

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 sostanza, si desidera "visitare" il "focus" dell'obiettivo con la funzione di wrap e quindi ricostruire il tipo "intero".

Lente e prisma

A Lens' sa significa che puoi sempre trovare un a all'interno di qualsiasi s . Un Prism' sa significa che a volte è possibile scoprire che s in realtà solo è a ma a volte è qualcosa d'altro.

Per essere più chiari, abbiamo _1 :: Lens' (a, b) a perché ogni tupla ha sempre un primo elemento. Abbiamo _Just :: Prism' (Maybe a) a perché a volte Maybe a è in realtà un a valore di avvolto in Just ma a volte è Nothing .

Con questa intuizione, alcuni combinatori standard possono essere interpretati in parallelo tra loro

  • view :: Lens' sa -> (s -> a) "ottiene" l' a della s
  • set :: Lens' sa -> (a -> s -> s) "set" la a fessura nella s
  • review :: Prism' sa -> (a -> s) "realizza" che un a potrebbe essere un s
  • preview :: Prism' sa -> (s -> Maybe a) "tentativi" per trasformare una s in una a .

Un altro modo di pensarci è che un valore di tipo Lens' sa dimostra che s ha la stessa struttura di (r, a) per alcuni r sconosciuti. D'altra parte, Prism' sa dimostra che s ha la stessa struttura di Either ra per alcuni r . Possiamo scrivere queste quattro funzioni sopra con questa conoscenza:

-- `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

attraversamenti

A Traversal' sa mostra che s ha 0-a-many a s al suo interno.

toListOf :: Traversal' s a -> (s -> [a])

Qualsiasi tipo t che è Traversable ha automaticamente quella traverse :: Traversal (ta) a .

Possiamo usare un Traversal per impostare o mappare più di tutti questi a valori

> 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]

Un f :: Lens' sa dice che c'è esattamente un a interno di s . A g :: Prism' ab dice che ci sono 0 o 1 b s in a . Comporre f . g ci dà un Traversal' sb perché seguendo f e poi g mostra come ci sono 0-a-1 b s in s .

Le lenti compongono

Se hai una f :: Lens' ab e una g :: Lens' bc allora f . g è un Lens' ac ottenuto seguendo prima f e poi g . In particolare:

  • Lenti a comporre come funzioni (in realtà solo che sono funzioni)
  • Se si pensa alla funzionalità di view di Lens , sembra che i flussi di dati "da sinistra a destra" - questo potrebbe risentire del normale intuito per la composizione delle funzioni. D'altra parte, dovrebbe essere naturale se ci pensi . -notazione come succede nelle lingue OO.

Più che comporre Lens with Lens , (.) Può essere usato per comporre quasi tutti i tipi " Lens like" insieme. Non è sempre facile vedere quale sia il risultato dal momento che il tipo diventa più difficile da seguire, ma è possibile utilizzare il grafico lens per capirlo. La composizione x . y ha il tipo di limite inferiore superiore dei tipi di x y in quel grafico.

Lenti classiche

Oltre alla funzione standard makeLenses per la generazione di Lens , Control.Lens.TH offre anche la funzione makeClassy . makeClassy ha lo stesso tipo e funziona essenzialmente allo stesso modo di makeLenses , con una differenza chiave. Oltre a generare gli obiettivi e gli attraversamenti standard, se il tipo non ha argomenti, creerà anche una classe che descrive tutti i tipi di dati che possiedono il tipo come un campo. Per esempio

data Foo = Foo { _fooX, _fooY :: Int }
  makeClassy ''Foo

creerà

class HasFoo t where
   foo :: Simple Lens t Foo

instance HasFoo Foo where foo = id

fooX, fooY :: HasFoo t => Simple Lens t Int

Campi con campi di applicazione

(Questo esempio è stato copiato da questa risposta StackOverflow )

Supponiamo che tu abbia un numero di tipi di dati diversi che tutti dovrebbero avere un obiettivo con lo stesso nome, in questo caso capacity . La slice di makeFields creerà una classe che lo compie senza conflitti makeFields spazio dei nomi.

{-# 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)

Quindi 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

Quindi, ciò che viene effettivamente fatto è dichiarato una classe HasCapacity sa , dove la capacità è un Lens' da s a a ( a viene corretto una volta s è noto). Ha capito il nome "capacità" rimuovendo il nome (in minuscolo) del tipo di dati dal campo; Trovo piacevole non dover utilizzare un carattere di sottolineatura sul nome del campo o sul nome dell'obiettivo, poiché a volte la sintassi del record è in realtà ciò che si desidera. È possibile utilizzare makeFieldsWith e le varie lenti lens per avere alcune opzioni diverse per il calcolo dei nomi dell'obiettivo.

Nel caso in cui aiuti, usando 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.

Quindi la prima giuntura rese la classe HasCapcity e aggiunse un'istanza per Foo; il secondo utilizzava la classe esistente e creava un'istanza per Bar.

Questo funziona anche se si importa la classe HasCapcity da un altro modulo; makeFields può aggiungere più istanze alla classe esistente e diffondere i tuoi tipi su più moduli. Ma se lo usi di nuovo in un altro modulo in cui non hai importato la classe, diventerà una nuova classe (con lo stesso nome) e avrai due obiettivi con capacità di sovraccarico separate che non sono compatibili.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow