soczewki, etykiety fc, akcesoria do danych - która biblioteka dostępu do struktury i mutacji jest lepsza

173

Istnieją co najmniej trzy popularne biblioteki umożliwiające dostęp do pól rekordów i manipulowanie nimi. Te, które znam, to: akcesorium do danych, etykiety fc i soczewki.

Osobiście zacząłem od akcesora danych i używam ich teraz. Jednak ostatnio w haskell-cafe panowała opinia, że ​​fclabels są lepsze.

Dlatego interesuje mnie porównanie tych trzech (a może i więcej) bibliotek.

Tener
źródło
3
Na dzień dzisiejszy lenspakiet ma najbogatszą funkcjonalność i dokumentację, więc jeśli nie masz nic przeciwko jego złożoności i zależnościom, to jest droga do zrobienia.
modułowe

Odpowiedzi:

200

Są co najmniej 4 biblioteki, o których wiem, że dostarczają soczewki.

Pojęcie soczewki polega na tym, że zapewnia ona coś izomorficznego

data Lens a b = Lens (a -> b) (b -> a -> a)

zapewniając dwie funkcje: pobierającą i ustawiającą

get (Lens g _) = g
put (Lens _ s) = s

podlega trzem prawom:

Po pierwsze, jeśli coś włożysz, możesz to z powrotem wyciągnąć

get l (put l b a) = b 

Po drugie, otrzymanie, a następnie ustawienie nie zmienia odpowiedzi

put l (get l a) a = a

Po trzecie, postawienie dwa razy jest tym samym, co postawienie raz, a raczej drugie postawienie wygrywa.

put l b1 (put l b2 a) = put l b1 a

Zwróć uwagę, że system typów nie jest wystarczający, aby sprawdzić te przepisy za Ciebie, więc musisz się upewnić, że niezależnie od zastosowanej implementacji obiektywu.

Wiele z tych bibliotek zapewnia również kilka dodatkowych kombinatorów na górze i zwykle jakąś formę szablonów maszyn haskell do automatycznego generowania soczewek dla pól prostych typów rekordów.

Mając to na uwadze, możemy przejść do różnych implementacji:

Wdrożenia

fclabels

fclabels jest prawdopodobnie najłatwiejszym argumentem na temat bibliotek soczewek, ponieważ a :-> bmożna go bezpośrednio przełożyć na powyższy typ. Udostępnia instancję Category,(:->) która jest przydatna, ponieważ umożliwia komponowanie soczewek. Zapewnia również Pointtyp bezprawia, który uogólnia pojęcie zastosowanej tutaj soczewki i pewną hydraulikę do radzenia sobie z izomorfizmami.

Jedną z przeszkód w przyjęciu programu fclabelsjest to, że główny pakiet zawiera instalację wodno-kanalizacyjną template-haskell, więc pakiet nie jest Haskell 98, a także wymaga (raczej niekontrowersyjnego) TypeOperatorsrozszerzenia.

akcesor danych

[Edytuj: data-accessornie używa już tej reprezentacji, ale przeszedł do formy podobnej do tej z data-lens. Ale zachowuję ten komentarz.]

Data-accessor jest nieco bardziej popularny niż fclabels, po części, dlatego, że jest to Haskell 98. Jednak jego wybór wewnętrznej reprezentacji sprawia, że ​​wymiotuję trochę w ustach.

Typ Tużywany do reprezentowania soczewki jest wewnętrznie definiowany jako

newtype T r a = Cons { decons :: a -> r -> (a, r) }

W związku z tym, aby getokreślić wartość soczewki, musisz podać nieokreśloną wartość dla argumentu „a”! Wydaje mi się to niesamowicie brzydką implementacją ad hoc.

To powiedziawszy, Henning włączył hydraulikę template-haskell, aby automatycznie generować akcesory w osobnym pakiecie „ data-accessor-template ”.

Ma tę zaletę, że jest to całkiem duży zestaw pakietów, które już go wykorzystują, jest to Haskell 98 i zapewnia najważniejszą Categoryinstancję, więc jeśli nie zwracasz uwagi na to, jak powstaje kiełbasa, ten pakiet jest w rzeczywistości całkiem rozsądnym wyborem .

soczewki

Następnie jest pakiet soczewek , który zauważa, że ​​soczewka może zapewnić homomorfizm monady stanu między dwiema monadami stanu, poprzez bezpośrednie definiowanie soczewek jako takich homomorfizmów monad.

Gdyby rzeczywiście zadał sobie trud dostarczenia typu dla swoich soczewek, miałyby typ drugiego stopnia, taki jak:

newtype Lens s t = Lens (forall a. State t a -> State s a)

W rezultacie raczej nie podoba mi się to podejście, ponieważ niepotrzebnie wyciąga cię z Haskell 98 (jeśli chcesz, aby typ dostarczał twoim soczewkom w abstrakcji) i pozbawia cię Categoryinstancji soczewek, które pozwolą ci skomponuj je z .. Implementacja wymaga również klas typu wieloparametrowego.

Należy pamiętać, że wszystkie inne biblioteki obiektywów wymienione tutaj zapewniają jakiś kombinator lub mogą być użyte do zapewnienia tego samego efektu fokalizacji stanu, więc nic nie jest zyskiwane przez bezpośrednie kodowanie obiektywu w ten sposób.

Ponadto warunki poboczne podane na początku nie mają ładnego wyrażenia w tej formie. Podobnie jak w przypadku „fclabels”, zapewnia to metodę template-haskell do automatycznego generowania soczewek dla typu rekordu bezpośrednio w głównym pakiecie.

Ze względu na brak Categoryinstancji, barokowe kodowanie i wymóg template-haskell w głównym pakiecie, jest to moja najmniej ulubiona implementacja.

soczewka danych

[Edycja: od 1.8.0 te zostały przeniesione z pakietu comonad-transformers do data-lens]

W moim data-lenspakiecie są soczewki w ramach comonad Sklepu .

newtype Lens a b = Lens (a -> Store b a)

gdzie

data Store b a = Store (b -> a) b

Rozszerzony jest to odpowiednik

newtype Lens a b = Lens (a -> (b, b -> a))

Można to postrzegać jako uwzględnienie wspólnego argumentu pobierającego i ustawiającego w celu zwrócenia pary składającej się z wyniku pobrania elementu i metody ustawiającej, która wstawia nową wartość. Daje to obliczeniową korzyść, jaką „ustawiacz” tutaj można ponownie wykorzystać część pracy użytej do uzyskania wartości, co zapewnia bardziej wydajną operację „modyfikowania” niż w fclabelsdefinicji, zwłaszcza gdy akcesoria są połączone łańcuchami.

Istnieje również dobre teoretyczne uzasadnienie tego przedstawienia, ponieważ podzbiór wartości „soczewki”, które spełniają 3 prawa określone na początku tej odpowiedzi, to dokładnie te soczewki, dla których funkcja opakowana jest „komonadą węgla” dla comonady sklepowej . To przekształca 3 włochate prawa dla obiektywu ldo 2 ładnie pozbawionych punktów odpowiedników:

extract . l = id
duplicate . l = fmap l . l

Podejście to pierwszy zauważył i opisał w Russell O'Connora Functorjest Lensjak Applicativejest Biplate: Przedstawiamy wielotarczowe i został blogu o oparciu o preprintu przez Jeremy Gibbons.

Zawiera również szereg kombinatorów do ścisłej pracy z soczewkami i niektóre standardowe soczewki do pojemników, takie jak Data.Map.

Tak więc soczewki w data-lensformie a Category(w przeciwieństwie do lensesopakowania) są Haskell 98 (w przeciwieństwie do fclabels/ lenses), są rozsądne (w przeciwieństwie do tylnej części data-accessor) i zapewniają nieco bardziej wydajną implementację, data-lens-fdzapewniają funkcjonalność do pracy z MonadState dla tych, którzy chcą wyjść na zewnątrz Haskell 98, a maszyneria template-haskell jest teraz dostępna za pośrednictwem data-lens-template.

Aktualizacja 28.06.2012: Inne strategie wdrażania obiektywów

Soczewki izomorficzne

Warto wziąć pod uwagę dwa inne kodowania obiektywów. Pierwsza daje dobry teoretyczny sposób spojrzenia na soczewkę jako sposób na rozbicie struktury na wartość pola i „wszystko inne”.

Podano typ izomorfizmów

data Iso a b = Iso { hither :: a -> b, yon :: b -> a }

takie, które spełniają ważni członkowie hither . yon = id, iyon . hither = id

Możemy przedstawić soczewkę z:

data Lens a b = forall c. Lens (Iso a (b,c))

Są one przede wszystkim przydatne jako sposób myślenia o znaczeniu soczewek i możemy ich użyć jako narzędzia do rozumowania, aby wyjaśnić inne soczewki.

Soczewki van Laarhoven

Możemy modelować soczewki w taki sposób, aby można je było komponować z, (.)a idnawet bez Categoryinstancji za pomocą

type Lens a b = forall f. Functor f => (b -> f b) -> a -> f a

jako typ naszych soczewek.

W takim razie zdefiniowanie soczewki jest tak proste, jak:

_2 f (a,b) = (,) a <$> f b

i możesz samodzielnie przekonać się, że skład funkcji to skład soczewki.

Niedawno pisałem o tym, jak można dalej uogólniać soczewki van Laarhovena, aby uzyskać rodziny soczewek, które mogą zmieniać typy pól, po prostu uogólniając ten podpis na

type LensFamily a b c d = forall f. Functor f => (c -> f d) -> a -> f b

Ma to niefortunną konsekwencję, że najlepszym sposobem mówienia o soczewkach jest użycie polimorfizmu rangi 2, ale nie musisz używać tego podpisu bezpośrednio podczas definiowania soczewek.

LensZdefiniowałem powyżej _2jest faktycznie LensFamily.

_2 :: Functor f => (a -> f b) -> (c,a) -> f (c, b)

Napisałem bibliotekę zawierającą soczewki, rodziny soczewek i inne uogólnienia, w tym gettery, setery, fałdy i przejścia. Jest dostępny na hackage jako lenspakiet.

Ponownie, wielką zaletą tego podejścia jest to, że opiekunowie bibliotek mogą faktycznie tworzyć soczewki w tym stylu w twoich bibliotekach bez ponoszenia jakiejkolwiek zależności od biblioteki soczewek, po prostu dostarczając funkcjom typ Functor f => (b -> f b) -> a -> f a, dla ich poszczególnych typów „a” i „b”. To znacznie obniża koszt adopcji.

Ponieważ nie musisz faktycznie używać pakietu do definiowania nowych obiektywów, znacznie odciąża to moje wcześniejsze obawy dotyczące zachowania biblioteki Haskell 98.

Edward KMETT
źródło
28
Podoba mi się fclabels za optymistyczne podejście:->
Tener
10
Czy zgodność z Haskell 1998 jest ważna? Ponieważ ułatwia tworzenie kompilatorów? I czy nie powinniśmy zamiast tego przejść do rozmowy o Haskell 2010?
yairchu
55
O nie! Byłem oryginalnym autorem data-accessor, a potem przekazałem go Henningowi i przestałem zwracać na to uwagę. a -> r -> (a,r)Reprezentacja również sprawia mi nieswojo, a moja oryginalna realizacja była jak twój Lenstyp. Heeennnninngg !!
luqui
5
Yairchu: głównie dlatego, że Twoja biblioteka może mieć szansę pracy z innym kompilatorem niż ghc. Nikt inny nie ma szablonu Haskell. Rok 2010 nie dodaje tu nic istotnego.
Edward KMETT