Próbuję i nie udaje mi się zrozumieć traverse
funkcji z Data.Traversable
. Nie widzę sensu. Ponieważ pochodzę z imperatywnego środowiska, czy ktoś mógłby mi to wyjaśnić za pomocą imperatywnej pętli? Pseudokod byłby bardzo mile widziany. Dzięki.
Próbuję i nie udaje mi się zrozumieć traverse
funkcji z Data.Traversable
. Nie widzę sensu. Ponieważ pochodzę z imperatywnego środowiska, czy ktoś mógłby mi to wyjaśnić za pomocą imperatywnej pętli? Pseudokod byłby bardzo mile widziany. Dzięki.
Odpowiedzi:
traverse
jest taki sam, jak fmap
, z wyjątkiem tego, że umożliwia również uruchamianie efektów podczas odbudowywania struktury danych.
Spójrz na przykład z Data.Traversable
dokumentacji.
data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)
Functor
Instancja Tree
będzie:
instance Functor Tree where
fmap f Empty = Empty
fmap f (Leaf x) = Leaf (f x)
fmap f (Node l k r) = Node (fmap f l) (f k) (fmap f r)
Odbudowuje całe drzewo, stosując się f
do każdej wartości.
instance Traversable Tree where
traverse f Empty = pure Empty
traverse f (Leaf x) = Leaf <$> f x
traverse f (Node l k r) = Node <$> traverse f l <*> f k <*> traverse f r
Traversable
Instancja jest prawie taka sama, z wyjątkiem konstruktorzy są nazywane w aplikacyjnej stylu. Oznacza to, że przy odbudowie drzewa możemy mieć (uboczne) efekty. Applicative jest prawie taki sam jak monady, z tym wyjątkiem, że efekty nie mogą zależeć od poprzednich wyników. W tym przykładzie oznacza to, że nie można było zrobić czegoś innego z prawą gałęzią węzła w zależności od wyników przebudowy na przykład lewej gałęzi.
Ze względów historycznych Traversable
klasa zawiera również monadyczną wersję traverse
tzw mapM
. Pod każdym względem mapM
jest to samo co traverse
- istnieje jako odrębna metoda, bo Applicative
dopiero później stała się nadklasą Monad
.
Jeśli zaimplementowałbyś to w nieczystym języku, fmap
byłoby to samo co traverse
, ponieważ nie ma sposobu, aby zapobiec efektom ubocznym. Nie możesz zaimplementować go jako pętli, ponieważ musisz rekurencyjnie przechodzić przez strukturę danych. Oto mały przykład, jak zrobiłbym to w JavaScript:
Node.prototype.traverse = function (f) {
return new Node(this.l.traverse(f), f(this.k), this.r.traverse(f));
}
Implementacja tego w ten sposób ogranicza cię do efektów, na które pozwala język. Jeśli wolisz niedeterminizm (który jest listą instancji modeli aplikacyjnych), a Twój język nie ma go wbudowanego, nie masz szczęścia.
Functor
części, która nie jest parametryczna. Wartość stanu w State
, awaria Maybe
i Either
, liczba elementów w []
i oczywiście dowolne zewnętrzne skutki uboczne w IO
. Nie obchodzi mnie to jako termin ogólny (podobnie jak Monoid
funkcje używające „pustego” i „dołączania”, koncepcja jest bardziej ogólna, niż sugeruje termin na początku), ale jest dość powszechna i wystarczająco dobrze służy celowi.
ap
poprzednich wyników. Odpowiednio przeredagowałem tę uwagę.
traverse
zamienia rzeczy wewnątrz a Traversable
w Traversable
rzeczy „wewnątrz” an Applicative
, biorąc pod uwagę funkcję, która tworzy Applicative
s z rzeczy.
Użyjmy Maybe
jako Applicative
i wypiszmy jako Traversable
. Najpierw potrzebujemy funkcji transformacji:
half x = if even x then Just (x `div` 2) else Nothing
Więc jeśli liczba jest parzysta, otrzymujemy jej połowę (wewnątrz a Just
), w przeciwnym razie otrzymamy Nothing
. Jeśli wszystko idzie „dobrze”, wygląda to tak:
traverse half [2,4..10]
--Just [1,2,3,4,5]
Ale...
traverse half [1..10]
-- Nothing
Powodem jest to, że <*>
funkcja jest używana do budowania wyniku, a gdy jeden z argumentów jest Nothing
, otrzymujemy z Nothing
powrotem.
Inny przykład:
rep x = replicate x x
Ta funkcja generuje listę długości x
z zawartością x
, np . rep 3
= [3,3,3]
. Jaki jest skutek traverse rep [1..3]
?
Otrzymujemy częściowe rezultaty [1]
, [2,2]
a [3,3,3]
przy użyciu rep
. Teraz semantyka list typu Applicatives
„weź wszystkie kombinacje”, np . (+) <$> [10,20] <*> [3,4]
Jest [13,14,23,24]
.
„Wszystkie kombinacje” [1]
i [2,2]
są dwa razy [1,2]
. Wszystkie kombinacje dwa razy [1,2]
i [3,3,3]
sześć razy [1,2,3]
. Więc mamy:
traverse rep [1..3]
--[[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]
fac n = length $ traverse rep [1..n]
liftA2 (,)
niż bardziej ogólny formularz traverse
.
Myślę, że najłatwiej jest to zrozumieć w kategoriach sequenceA
, co traverse
można zdefiniować następująco.
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
traverse f = sequenceA . fmap f
sequenceA
sekwencjonuje razem elementy struktury od lewej do prawej, zwracając strukturę o tym samym kształcie zawierającą wyniki.
sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a)
sequenceA = traverse id
Możesz również pomyśleć o sequenceA
odwróceniu kolejności dwóch funktorów, np. Przejściu z listy akcji do akcji zwracającej listę wyników.
Tak więc traverse
przyjmuje pewną strukturę i stosuje się f
do przekształcenia każdego elementu w strukturze w jakąś aplikację, a następnie sekwencjonuje efekty tych aplikacji od lewej do prawej, zwracając strukturę o tym samym kształcie zawierającą wyniki.
Możesz również porównać to z Foldable
, które definiuje powiązaną funkcję traverse_
.
traverse_ :: (Foldable t, Applicative f) => (a -> f b) -> t a -> f ()
Widać więc, że kluczowa różnica między Foldable
i Traversable
polega na tym, że ta ostatnia pozwala zachować kształt konstrukcji, podczas gdy pierwsza wymaga złożenia wyniku w inną wartość.
Prostym przykładem jego użycia jest użycie listy jako przejezdnej struktury i IO
aplikacji:
λ> import Data.Traversable
λ> let qs = ["name", "quest", "favorite color"]
λ> traverse (\thing -> putStrLn ("What is your " ++ thing ++ "?") *> getLine) qs
What is your name?
Sir Lancelot
What is your quest?
to seek the holy grail
What is your favorite color?
blue
["Sir Lancelot","to seek the holy grail","blue"]
Chociaż ten przykład jest raczej mało ekscytujący, rzeczy stają się bardziej interesujące, gdy traverse
jest używany na innych typach pojemników lub przy użyciu innych aplikacji.
sequenceA . fmap
ponieważ listy jest równoważne, sequence . map
prawda?
IO
Typ może być użyty do wyrażenia efektów ubocznych; (2) IO
jest monadą, co okazuje się bardzo wygodne. Monady nie są zasadniczo powiązane z efektami ubocznymi. Należy również zauważyć, że istnieje znaczenie „skutku”, które jest szersze niż „efekt uboczny” w jego zwykłym znaczeniu - obejmującym czyste obliczenia. W tym ostatnim punkcie zobacz także Co dokładnie oznacza „skuteczny” .
To trochę tak fmap
, z wyjątkiem tego, że możesz uruchamiać efekty wewnątrz funkcji mapper, która również zmienia typ wyniku.
Wyobraźmy sobie listę liczb całkowitych reprezentujących identyfikatory użytkowników w bazie danych: [1, 2, 3]
. Jeśli chcesz, aby fmap
te identyfikatory użytkowników były nazwami użytkowników, nie możesz użyć tradycyjnego fmap
, ponieważ wewnątrz funkcji musisz uzyskać dostęp do bazy danych, aby odczytać nazwy użytkowników (co wymaga efektu - w tym przypadku za pomocą IO
monady).
Podpis traverse
:
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
Dzięki traverse
, możesz robić efekty, dlatego twój kod do mapowania identyfikatorów użytkowników na nazwy użytkowników wygląda następująco:
mapUserIDsToUsernames :: (Num -> IO String) -> [Num] -> IO [String]
mapUserIDsToUsernames fn ids = traverse fn ids
Jest też funkcja o nazwie mapM
:
mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
Każde użycie mapM
może zostać zastąpione traverse
, ale nie odwrotnie. mapM
działa tylko dla monad, podczas gdy traverse
jest bardziej ogólny.
Jeśli chcesz tylko uzyskać efekt i nie zwracać żadnej użytecznej wartości, istnieją traverse_
i mapM_
wersje tych funkcji, z których obie ignorują wartość zwracaną przez funkcję i są nieco szybsze.
traverse
jest pętlą. Jego implementacja zależy od struktury danych, przez którą przechodzimy. To może być lista, drzewo Maybe
, Seq
(uence), lub cokolwiek, co ma ogólny sposób przejeżdżającego przez coś jak dla pętli lub funkcji rekurencyjnej. Tablica zawierałaby pętlę for, listę pętlę while, drzewo albo coś rekurencyjnego, albo połączenie stosu z pętlą while; ale w językach funkcyjnych nie potrzebujesz tych uciążliwych poleceń pętli: łączysz wewnętrzną część pętli (w kształcie funkcji) ze strukturą danych w sposób bardziej bezpośredni i mniej rozwlekły.
Dzięki Traversable
typeklasie prawdopodobnie mógłbyś napisać swoje algorytmy bardziej niezależne i wszechstronne. Ale moje doświadczenie mówi, że Traversable
jest to zwykle używane tylko do prostego przyklejenia algorytmów do istniejących struktur danych. Miło jest nie pisać podobnych funkcji dla różnych kwalifikowanych typów danych.