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

  1. Wyodrębnij a , zapominając o dodatkowym kontekście lub
  2. 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ę.

wprowadź opis zdjęcia tutaj

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' lub Simple 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 poza s
  • set :: Lens' sa -> (a -> s -> s) "zestawy" the gniazdo w a s
  • review :: Prism' sa -> (a -> s) „zdaje sobie sprawę”, że a może być s
  • preview :: Prism' sa -> (s -> Maybe a) „próbuje” zamienić s w a .

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 funkcjami)
  • Jeśli pomyślisz o funkcji view w Lens , 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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow