Oferują czystą abstrakcję aktualizacji danych i nigdy nie są naprawdę "potrzebne". Po prostu pozwalali ci rozumować o problemie w inny sposób.
W niektórych imperatywnych/"obiektowych" językach programowania, takich jak C, masz znaną koncepcję pewnego zbioru wartości (nazwijmy je "strukturami") i sposoby oznaczania każdej wartości w kolekcji (etykiety są zwykle nazywane "polami").Prowadzi to do definicji takich jak to:
typedef struct { /* defining a new struct type */
float x; /* field */
float y; /* field */
} Vec2;
typedef struct {
Vec2 col1; /* nested structs */
Vec2 col2;
} Mat2;
Następnie można utworzyć wartości tego nowo zdefiniowanego typu tak:
Vec2 vec = { 2.0f, 3.0f };
/* Reading the components of vec */
float foo = vec.x;
/* Writing to the components of vec */
vec.y = foo;
Mat2 mat = { vec, vec };
/* Changing a nested field in the matrix */
mat.col2.x = 4.0f;
Podobnie w Haskell, mamy typy danych:
data Vec2 =
Vec2
{ vecX :: Float
, vecY :: Float
}
data Mat2 =
Mat2
{ matCol1 :: Vec2
, matCol2 :: Vec2
}
Ten typ danych jest następnie używany w następujący sposób:
let vec = Vec2 2 3
-- Reading the components of vec
foo = vecX vec
-- Creating a new vector with some component changed.
vec2 = vec { vecY = foo }
mat = Mat2 vec2 vec2
Jednak w Haskell nie ma prostego sposobu na zmianę zagnieżdżonych pól w strukturze danych. Dzieje się tak dlatego, że musisz ponownie utworzyć wszystkie obiekty zawijania wokół wartości, którą zmieniasz, ponieważ wartości Haskella są niezmienne. Jeśli masz matrycę jak wyżej w Haskell, i chcesz zmienić prawy górny komórkę w macierzy, trzeba napisać to:
mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }
To działa, ale wygląda niezdarny. To, co ktoś wymyślił, jest w zasadzie następujące: Jeśli pogrupujesz dwie rzeczy: "getter" wartości (np. vecX
i matCol2
powyżej) z odpowiednią funkcją, która, biorąc pod uwagę strukturę danych, do której należy getter, może Utwórz nową strukturę danych z tą wartością, możesz zrobić wiele fajnych rzeczy. Na przykład:
data Data = Data { member :: Int }
-- The "getter" of the member variable
getMember :: Data -> Int
getMember d = member d
-- The "setter" or more accurately "updater" of the member variable
setMember :: Data -> Int -> Data
setMember d m = d { member = m }
memberLens :: (Data -> Int, Data -> Int -> Data)
memberLens = (getMember, setMember)
Istnieje wiele sposobów wdrażania soczewek; w tym tekście powiedzmy, że soczewka jest podobna do powyższej:
type Lens a b = (a -> b, a -> b -> a)
tj. jest to kombinacja gettera i setera dla jakiegoś typu a
, który ma pole typu b
, więc memberLens
powyżej byłby Lens Data Int
. Co to pozwala nam robić?
Cóż, najpierw zrobić dwie proste funkcje, które wyodrębnić pobierające i ustawiające z obiektywem:
getL :: Lens a b -> a -> b
getL (getter, setter) = getter
setL :: Lens a b -> a -> b -> a
setL (getter, setter) = setter
Teraz możemy zacząć abstrahując na rzeczy. Przyjrzyjmy się sytuacji powyżej, że chcemy zmodyfikować wartość "głęboko na dwa piętra". Dodamy strukturę danych z innego obiektywu:
data Foo = Foo { subData :: Data }
subDataLens :: Lens Foo Data
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition
Teraz dodajmy funkcję, która komponuje dwa obiektywy:
(#) :: Lens a b -> Lens b c -> Lens a c
(#) (getter1, setter1) (getter2, setter2) =
(getter2 . getter1, combinedSetter)
where
combinedSetter a x =
let oldInner = getter1 a
newInner = setter2 oldInner x
in setter1 a newInner
Kod jest rodzaj szybko napisany, ale myślę, że to jasne, co robi : pobierający są po prostu złożeni; otrzymujesz wewnętrzną wartość danych, a następnie czytasz jej pole. Seter, gdy ma zmienić wartość a
z nową wartością pola wewnętrznego x
, najpierw pobiera starą wewnętrzną strukturę danych, ustawia swoje wewnętrzne pole, a następnie aktualizuje zewnętrzną strukturę danych z nową wewnętrzną strukturą danych.
Teraz zróbmy funkcję, która po prostu zwiększa wartość obiektywu:
increment :: Lens a Int -> a -> a
increment l a = setL l a (getL l a + 1)
Jeśli mamy tego kodu, staje się jasne, co robi:
d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.
teraz, bo potrafimy komponować soczewki, możemy to również zrobić:
f = Foo (Data 5)
print $ increment (subDataLens#memberLens) f
-- Prints "Foo (Data 6)", the innermost field is updated.
Co wszystkie pakiety soczewek zrobić, to w rapuj tę koncepcję soczewek - zgrupowanie "setera" i "gettera" w zgrabny pakiet, który sprawia, że są łatwe w użyciu. W konkretnej implementacji soczewek można by napisać:
with (Foo (Data 5)) $ do
subDataLens . memberLens $= 7
Tak więc, bardzo zbliżyłeś się do wersji C kodu; bardzo łatwo modyfikuje zagnieżdżone wartości w drzewie struktur danych.
Obiektywy to nic innego jak prosty sposób na modyfikowanie części niektórych danych. Ponieważ o wiele łatwiej jest wnioskować o pewnych koncepcjach z ich powodu, widzą one szerokie zastosowanie w sytuacjach, w których masz ogromne zbiory struktur danych, które muszą na różne sposoby współdziałać ze sobą.
Informacje o zaletach i wadach obiektywów można znaleźć na stronie a recent question here on SO.
Możesz cieszyć się oglądaniem rozmowy Edwarda Kmetta [Obiektywy: A Functional Imperative] (http://www.youtube.com/watch?v=efv0SQNde5Q). Jest on przedstawiony w Scali, ale tłumaczenie przydatności soczewek w Haskell powinno być jasne. –