Haskell Language
lente
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
- Estrarre la
a
dimenticando il contesto aggiuntivo o - 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.
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 dellaSimple 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
dellas
-
set :: Lens' sa -> (a -> s -> s)
"set" laa
fessura nellas
-
review :: Prism' sa -> (a -> s)
"realizza" che una
potrebbe essere uns
-
preview :: Prism' sa -> (s -> Maybe a)
"tentativi" per trasformare unas
in unaa
.
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
diLens
, 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.