2012-05-28 7 views
33

Nie mogę znaleźć żadnego wyjaśnienia, jakie soczewki są używane w praktycznych przykładach. Ten krótki akapit ze strony Hackage jest najbliższy, jaki znalazłem:Jakie soczewki są używane/przydatne dla?

Ten moduł zapewnia wygodny sposób uzyskiwania dostępu i aktualizowania elementów struktury. Jest bardzo podobny do Data.Accessors, ale nieco bardziej ogólny i ma mniej zależności. Szczególnie podoba mi się, jak czysto radzi sobie ze strukturami zagnieżdżonymi w stanach monad.

Do czego służą? Jakie są zalety i wady w stosunku do innych metod? Dlaczego są potrzebne?

+2

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. –

Odpowiedz

45

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.

+2

Jednym z ważnych punktów, na które brakuje twojej odpowiedzi, jest to, że soczewki są * pierwszą klasą *, więc możesz budować z nich inne abstrakcje. W tym przypadku wbudowana składnia rekordu kończy się niepowodzeniem. – jberryman

+2

Napisałem też wpis na blogu na temat soczewek, które mogą być przydatne dla OP: http://www.haskellforall.com/2012/01/haskell-for-mainstream-programmers_28.html –

12

Obiektywy zapewniają wygodne sposoby edycji danych w jednorodny, kompozycyjny sposób.

Wiele programów są wokół następujących operacji:

  • przeglądania komponent (ewentualnie zagnieżdżonej) strukturze danych
  • aktualizowanie pola (ewentualnie zagnieżdżonych) struktur danych

Soczewki zapewniają obsługa języków do przeglądania i edycji struktur w sposób zapewniający spójność wprowadzanych zmian; że edycje można łatwo skomponować; i że ten sam kod może być użyty do oglądania części struktury, tak jak do aktualizacji części struktury.

Obiektywy ułatwiają pisanie programów z widoków na struktury; i od struktur z powrotem do widoków (i edytorów) dla tych struktur. Sprzątają dużo bałaganu wśród rekordzistów i seterów.

Pierce i in. spopularyzowane soczewki, np. w ich wersjach Quotient Lenses paper i implementacje dla Haskella są obecnie szeroko stosowane (na przykład fclabels i akcesory danych).

Dla przypadków użycia betonu, należy rozważyć:

  • graficznych interfejsów użytkownika, w którym użytkownik edytuje informacji w sposób ustrukturyzowany
  • parsera i ładne drukarek
  • kompilatory
  • synchronizujące aktualizacji struktur danych
  • bazy danych i schematy

i wiele innych sytuacji, w których istnieje model struktury danych na świecie oraz widok edytowalny na te dane.

6

Jako dodatkową uwagę często pomija się fakt, że soczewki realizują bardzo ogólne pojęcie "dostępu do pola i aktualizacji". Obiektywy można zapisywać dla różnych rzeczy, w tym obiektów funkcjonalnych. To wymaga trochę myślenia abstrakcyjnego, aby docenić to, więc pokażę wam przykład mocy soczewek:

at :: (Eq a) => a -> Lens (a -> b) b 

Korzystanie at rzeczywiście można uzyskać dostęp i manipulować funkcje z wielu argumentów w zależności od wcześniejszych argumentów. Pamiętaj, że Lens to kategoria. Jest to bardzo przydatny język do lokalnie dostosowujących funkcje lub inne rzeczy.

Można także uzyskać dostęp do danych o właściwościach lub alternatywne reprezentacje:

polar :: (Floating a, RealFloat a) => Lens (Complex a) (a, a) 
mag :: (RealFloat a) => Lens (Complex a) a 

Można pójść dalej pisać soczewek aby uzyskać dostęp do poszczególnych pasm o przekształconej Fouriera sygnału i wiele więcej.