Haskell Language
Obiektyw
Szukaj…
Wprowadzenie
Lens to biblioteka dla Haskell, która zapewnia soczewki, izomorfizmy, fałdy, przejścia, gettery i setery, która udostępnia jednolity interfejs do zapytań i manipulacji dowolnymi strukturami, podobnie jak koncepcje akcesora i mutatora Javy.
Uwagi
Co to jest obiektyw?
Soczewki (i inne elementy optyczne) pozwalają nam osobno opisywać, w jaki sposób chcemy uzyskać dostęp do niektórych danych od tego , co chcemy z nimi zrobić. Ważne jest, aby odróżnić abstrakcyjne pojęcie soczewki od konkretnej implementacji. Zrozumienie abstrakcyjne znacznie ułatwia programowanie przy użyciu lens
na dłuższą metę. Istnieje wiele izomorficznych przedstawień soczewek, więc w tej dyskusji unikniemy jakiejkolwiek konkretnej dyskusji na temat implementacji, a zamiast tego przedstawimy ogólny przegląd pojęć.
Skupienie
Ważnym pojęciem w abstrakcyjnym rozumieniu jest pojęcie skupienia . Ważna optyka skupia się na określonej części większej struktury danych, nie zapominając o większym kontekście. Na przykład soczewka _1
skupia się na pierwszym elemencie krotki, ale nie zapomina o tym, co było w drugim polu.
Po skupieniu możemy porozmawiać o tym, jakie operacje możemy wykonywać za pomocą obiektywu. Biorąc pod uwagę Lens sa
który, gdy otrzyma typ danych typu s
skupia się na konkretnym a
, możemy albo
- Wyodrębnij
a
, zapominając o dodatkowym kontekście lub - Zamień
a
, podając nową wartość
Odpowiadają one dobrze znanym operacjom get
i set
, które są zwykle używane do scharakteryzowania obiektywu.
Inne elementy optyczne
W podobny sposób możemy rozmawiać o innych układach optycznych.
Optyczny | Skupiony na... |
---|---|
Obiektyw | Jedna część produktu |
Pryzmat | Jedna część sumy |
Traversal | Zero lub więcej części struktury danych |
Izomorfizm | ... |
Każda optyka skupia się w inny sposób, w zależności od tego, jaki rodzaj optyki mamy, możemy wykonywać różne operacje.
Kompozycja
Co więcej, możemy skomponować dowolną z dwóch optyków, które do tej pory omawialiśmy, aby określić złożony dostęp do danych. Cztery typy optyki, które omawialiśmy, tworzą sieć, wynik połączenia dwóch elementów optycznych stanowi ich górną granicę.
Na przykład, jeśli skomponujemy razem soczewkę i pryzmat, otrzymamy przejście. Powodem tego jest to, że ze względu na (pionowy) skład najpierw skupiamy się na jednej części produktu, a następnie na jednej części sumy. Rezultatem jest optyka, która koncentruje się na dokładnie zerowej lub jednej części naszych danych, co jest szczególnym przypadkiem przejścia. (Jest to również czasami nazywane przechodzeniem afinicznym).
W Haskell
Powodem popularności w Haskell jest to, że istnieje bardzo zwięzła reprezentacja optyki. Cała optyka to tylko funkcje pewnej postaci, które można skomponować razem, używając kompozycji funkcji. Prowadzi to do bardzo lekkiego osadzenia, co ułatwia integrację optyki z programami. Ponadto, ze względu na szczegóły kodowania, kompozycja funkcji automatycznie oblicza również górną granicę dwóch optyki, które tworzymy. Oznacza to, że możemy ponownie wykorzystywać te same kombinatory do różnych układów optycznych bez jawnego rzutowania.
Manipulowanie krotkami za pomocą obiektywu
Dostawać
("a", 1) ^. _1 -- returns "a"
("a", 1) ^. _2 -- returns 1
Oprawa
("a", 1) & _1 .~ "b" -- returns ("b", 1)
Modyfikacja
("a", 1) & _2 %~ (+1) -- returns ("a", 2)
both
Traversal
(1, 2) & both *~ 2 -- returns (2, 4)
Soczewki do rejestrów
Prosty zapis
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Point = Point {
_x :: Float,
_y :: Float
}
makeLenses ''Point
Soczewki x
i y
są tworzone.
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 }
Zarządzanie rekordami za pomocą powtarzających się nazw pól
data Person = Person { _personName :: String }
makeFields ''Person
Tworzy klasę typu HasName
, name
obiektywu dla Person
i czyni Person
instancją HasName
. Do klasy zostaną również dodane kolejne rekordy:
data Entity = Entity { _entityName :: String }
makeFields ''Entity
Rozszerzenie Haskell szablonu jest wymagane do działania makeFields
. Z technicznego punktu widzenia możliwe jest tworzenie soczewek wykonanych w ten sposób innymi sposobami, np. Ręcznie.
Soczewki stanowe
Operatorzy obiektywu mają przydatne warianty, które działają w kontekstach stanowych. Są one uzyskiwane przez zastąpienie ~
znakiem =
w nazwie operatora.
(+~) :: Num a => ASetter s t a a -> a -> s -> t
(+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()
Uwaga: Oczekuje się, że warianty stanowe nie zmienią typu, więc mają podpisy
Lens'
lubSimple Lens'
.
Pozbywanie się &
łańcuchy
Jeśli konieczne jest połączenie łańcuchowe operacji, często wygląda to tak:
change :: A -> A
change a = a & lensA %~ operationA
& lensB %~ operationB
& lensC %~ operationC
Działa to dzięki połączeniu &
. Wersja stanowa jest jednak jaśniejsza.
change a = flip execState a $ do
lensA %= operationA
lensB %= operationB
lensC %= operationC
Jeśli lensX
jest faktycznie id
, całą operację można oczywiście wykonać bezpośrednio, po prostu podnosząc ją za pomocą modify
.
Kod imperatywny ze stanem strukturalnym
Zakładając ten przykładowy stan:
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
Możemy pisać kod, który przypomina klasyczne języki imperatywne, a jednocześnie pozwala nam korzystać z zalet 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) %= ...
Pisanie obiektywu bez szablonu Haskell
Aby odszyfrować szablon Haskell, załóżmy, że masz
data Example a = Example { _foo :: Int, _bar :: a }
następnie
makeLenses 'Example
produkuje (mniej więcej)
foo :: Lens' (Example a) Int bar :: Lens (Example a) (Example b) a b
Nie dzieje się jednak nic szczególnie magicznego. Możesz je napisać samodzielnie:
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)
Zasadniczo chcesz „skupić się” na „ogniskowaniu” obiektywu za pomocą funkcji wrap
a następnie odbudować typ „cały”.
Obiektyw i pryzmat
A Lens' sa
oznacza, że można zawsze znaleźć a
w ciągu dowolnych s
. A Prism' sa
oznacza, że można czasami znaleźć że s
faktycznie tak jest ale czasami jest to coś innego. a
Aby być bardziej zrozumiałym, mamy _1 :: Lens' (a, b) a
ponieważ każda krotka zawsze ma pierwszy element. Mamy _Just :: Prism' (Maybe a) a
bo czasami Maybe a
to faktycznie wartość zawinięte w a
Just
, ale czasami jest to Nothing
.
Dzięki tej intuicji niektóre standardowe kombinatory można interpretować równolegle względem siebie
-
view :: Lens' sa -> (s -> a)
„dostaje”a
pozas
-
set :: Lens' sa -> (a -> s -> s)
"zestawy" the gniazdo wa
s
-
review :: Prism' sa -> (a -> s)
„zdaje sobie sprawę”, żea
może byćs
-
preview :: Prism' sa -> (s -> Maybe a)
„próbuje” zamienićs
wa
.
Innym sposobem, aby myśleć o tym, że wartość typu Lens' sa
pokazuje, że s
ma taką samą strukturę jak (r, a)
z nieznanych r
. Z drugiej strony, Prism' sa
pokazuje, że s
ma taką samą strukturę jak Either ra
dla niektórych r
. Dzięki tej wiedzy możemy napisać powyższe cztery funkcje:
-- `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
Traversal' sa
pokazuje, że s
ma w sobie od 0 do wielu a
.
toListOf :: Traversal' s a -> (s -> [a])
Każdy typ t
który jest Traversable
automatycznie ma taki traverse :: Traversal (ta) a
.
Możemy użyć Traversal
do zestawu lub map w ciągu tych wszystkich a
wartościami
> 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]
f :: Lens' sa
mówi, że dokładnie jedna wewnątrz a
s
. g :: Prism' ab
mówi, że są równe 0 lub 1, b
e w . a
Komponowanie f . g
daje nam s Traversal' sb
ponieważ po f
a następnie g
pokazuje, jak s są 0 do 1 b
s
.
Soczewki komponują
Jeśli masz f :: Lens' ab
i g :: Lens' bc
to f . g
to Lens' ac
uzyskany przez Lens' ac
wykonując najpierw f
a następnie g
. Szczególnie:
- Soczewki komponować jako funkcji (naprawdę po prostu są funkcjami)
- Jeśli pomyślisz o funkcji
view
wLens
, wygląda na to, że przepływy danych przebiegają „od lewej do prawej” - może się to wydawać odwrócone do normalnej intuicji w zakresie kompozycji funkcji. Z drugiej strony, powinno się wydawać naturalne.
-notacja jak to się dzieje w językach OO.
Więcej niż tylko komponowanie Lens
with Lens
, (.)
Może być użyte do skomponowania razem prawie każdego typu „ Lens
like”. Nie zawsze łatwo jest zobaczyć, jaki jest wynik, ponieważ typ staje się trudniejszy do naśladowania, ale możesz użyć wykresu lens
aby go zrozumieć. Kompozycja x . y
ma typ najniższej górnej granicy typów zarówno x
jak i y
na tym wykresie.
Eleganckie soczewki
Oprócz standardowej funkcji makeLenses
do generowania es Lens
, Control.Lens.TH
oferuje również funkcję makeClassy
. makeClassy
ma ten sam typ i działa zasadniczo tak samo jak makeLenses
, z jedną kluczową różnicą. Oprócz generowania standardowych soczewek i przejść, jeśli typ nie ma argumentów, utworzy także klasę opisującą wszystkie typy danych, które posiadają typ jako pole. Na przykład
data Foo = Foo { _fooX, _fooY :: Int }
makeClassy ''Foo
stworzy
class HasFoo t where
foo :: Simple Lens t Foo
instance HasFoo Foo where foo = id
fooX, fooY :: HasFoo t => Simple Lens t Int
Pola z makeFields
(Ten przykład skopiowany z tej odpowiedzi StackOverflow )
Załóżmy, że masz wiele różnych typów danych, z których wszystkie powinny mieć soczewki o tej samej nazwie, w tym przypadku capacity
. Plasterek makeFields
utworzy klasę, która osiągnie to bez konfliktów przestrzeni nazw.
{-# 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)
Następnie w 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
Tak więc to, co faktycznie zostało zrobione, jest zadeklarowane jako klasa HasCapacity sa
, gdzie pojemność to Lens'
od s
do a
( a
jest ustalane, gdy s jest znane). Odkrył nazwę „pojemność”, usuwając z pola (małe litery) nazwę typu danych; Uważam, że miło jest nie używać podkreślenia ani nazwy pola, ani nazwy obiektywu, ponieważ czasami składnia rekordu jest w rzeczywistości tym, czego chcesz. Możesz użyć makeFieldsWith i różnych lensRules, aby uzyskać różne opcje obliczania nazw soczewek.
Jeśli to pomaga, używając 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.
Tak więc pierwsze połączenie utworzyło klasę HasCapcity
i dodało instancję dla Foo; drugi użył istniejącej klasy i stworzył instancję dla Bar.
Działa to również, jeśli zaimportujesz klasę HasCapcity
z innego modułu; makeFields
może dodawać więcej instancji do istniejącej klasy i rozkładać typy na wiele modułów. Ale jeśli użyjesz go ponownie w innym module, do którego klasa nie została zaimportowana, utworzy nową klasę (o tej samej nazwie) i będziesz mieć dwa osobne przeciążone obiektywy, które nie są kompatybilne.