Хтось може пояснити траверсну функцію в Хаскелі?


99

Я намагаюся і не можу переглядати цю traverseфункцію Data.Traversable. Я не можу зрозуміти його сенс. Оскільки я походжу з імперативного походження, чи може хтось пояснити мені це термінами імперативного циклу? Псевдокод був би дуже вдячний. Дякую.


1
Стаття Суть візерунка ітератора може бути корисною, оскільки вона будує уявлення про крок за кроком. Хоча деякі передові концепції присутні
Джекі

Відповіді:


121

traverseте саме fmap, за винятком того, що він також дозволяє запускати ефекти під час перебудови структури даних.

Погляньте на приклад із Data.Traversableдокументації.

 data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)

FunctorПримірник Treeбуде:

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)

Він відновлює все дерево, застосовуючи fдо кожного значення.

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Примірник майже те ж саме, за винятком того, що конструктори викликаються в аплікативного стилі. Це означає, що ми можемо мати (побічні) ефекти під час перебудови дерева. Застосований майже такий самий, як і монади, за винятком того, що ефекти не можуть залежати від попередніх результатів. У цьому прикладі це означає, що ви не можете зробити щось інше, ніж права гілка вузла, залежно від результатів відновлення лівої гілки, наприклад.

З історичних причин, Traversableклас також містить одномісну версію traverseназивається mapM. Для всіх намірів і цілей mapMце те ж саме traverse- він існує як окремий метод, оскільки Applicativeлише згодом став суперкласом Monad.

Якщо ви реалізуєте це нечистою мовою, fmapце буде те саме traverse, що немає способу запобігти побічним ефектам. Ви не можете реалізувати це як цикл, оскільки вам доведеться пройти структуру даних рекурсивно. Ось невеликий приклад, як я це зробив би в Javascript:

Node.prototype.traverse = function (f) {
  return new Node(this.l.traverse(f), f(this.k), this.r.traverse(f));
}

Впроваджуючи це так, ви обмежуєте ефекти, які дозволяє мова. Якщо вам потрібна недетермінованість (яка є екземпляром списку застосувальних моделей), а у вашій мові вона не вбудована, вам не пощастило.


11
Що означає термін "ефект"?
missingfaktor

24
@missingfaktor: Це означає структурну інформацію про Functorчастину, яка не є параметричною. Значення стану в State, збій Maybeі Either, кількість елементів у [], і звичайно довільні зовнішні побічні ефекти в IO. Мені це байдуже як загальний термін (як Monoidфункції, що використовують "порожній" та "додати", концепція є загальнішою, ніж спочатку пропонується цим терміном), але вона досить поширена і слугує цілі досить добре.
CA McCann

@CA McCann: Зрозумів. Дякуємо за відповідь!
missingfaktor

1
"Я майже впевнений, що ти не повинен робити цього [...]." Однозначно ні - це було б настільки неприємно, як і apзалежність наслідків від попередніх результатів. Я переформулював це зауваження відповідно.
дуплод

2
"Застосовний майже такий самий, як і монади, за винятком того, що ефекти не можуть залежати від попередніх результатів." ... за допомогою цього рядка для мене клацнуло багато речей, дякую!
агам

58

traverseперетворює речі всередині a Traversableна a Traversableречей "всередині" an Applicative, задану функцію, яка робить Applicatives з речей.

Давайте використаємо Maybeas Applicativeі перелічимо як Traversable. Спочатку нам потрібна функція перетворення:

half x = if even x then Just (x `div` 2) else Nothing

Отже, якщо число парне, ми отримуємо половину його (всередині a Just), інакше отримуємо Nothing. Якщо все проходить "добре", це виглядає так:

traverse half [2,4..10]
--Just [1,2,3,4,5]

Але ...

traverse half [1..10]
-- Nothing

Причина полягає в тому, що <*>функція використовується для побудови результату, і коли є один із аргументів Nothing, ми Nothingповертаємось.

Інший приклад:

rep x = replicate x x

Ця функція генерує список довжин xіз вмістом x, наприклад rep 3= [3,3,3]. Який результат traverse rep [1..3]?

Ми отримуємо часткові результати [1], [2,2]і [3,3,3]використання rep. Тепер семантика списків як Applicativesє "приймати всі комбінації", наприклад (+) <$> [10,20] <*> [3,4]є [13,14,23,24].

"Усі комбінації" [1]і [2,2]складаються два рази [1,2]. Всі комбінації два рази [1,2]і [3,3,3]шість разів [1,2,3]. Отже, маємо:

traverse rep [1..3]
--[[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]

1
Ваш кінцевий результат мені про це нагадує .
hugomg

3
@missingno: Так, вони пропустилиfac n = length $ traverse rep [1..n]
Landei

1
Насправді, це є в "Програміст кодування списку" (але з використанням розуміння списку). Цей веб-сайт є вичерпним :)
hugomg

1
@missingno: Хм, це не зовсім однаково ... обидва покладаються на декартову поведінку продукту списку монад, але сайт використовує лише два одночасно, тож це більше схоже на виконання, liftA2 (,)ніж використання більш загальної форми traverse.
CA McCann

41

Я думаю, це найпростіше зрозуміти з точки зору sequenceA, як traverseце можна визначити наступним чином.

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
traverse f = sequenceA . fmap f

sequenceA послідовно поєднує елементи структури зліва направо, повертаючи структуру з однаковою формою, що містить результати.

sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a)
sequenceA = traverse id

Ви також можете розглядати sequenceAяк зворотний порядок двох функторів, наприклад перехід зі списку дій у дію, що повертає список результатів.

Отже, traverseприймає деяку структуру і застосовується fдля перетворення кожного елемента в структуру в якийсь застосувальний, а потім послідовно впливає на ефекти цих додатків зліва направо, повертаючи структуру з тією ж формою, що містить результати.

Ви також можете порівняти його з Foldable, який визначає відповідну функцію traverse_.

traverse_ :: (Foldable t, Applicative f) => (a -> f b) -> t a -> f ()

Отже, ви можете бачити, що ключова різниця між Foldableта Traversableполягає в тому, що остання дозволяє зберегти форму конструкції, тоді як перша вимагає скласти результат в якесь інше значення.


Простим прикладом його використання є використання списку як структури, IOщо проходить , і як додатка:

λ> 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"]

Хоча цей приклад досить захоплюючий, речі стають цікавішими, коли traverseвін використовується на контейнерах інших типів або з використанням інших додатків.


Отже, траверс - це просто більш загальна форма mapM? Насправді, sequenceA . fmapдля списків еквівалентно sequence . mapчи не так?
Raskell

Що ви маєте на увазі під "послідовністю побічних ефектів"? Що таке "побічний ефект" у вашій відповіді - я просто думав, що побічні ефекти можливі лише в монадах. З повагою
Марек

1
@Marek "Я просто думав, що побічні ефекти можливі лише в монадах". Зв'язок набагато вільніший, ніж такий: (1) IO тип можна використовувати для вираження побічних ефектів; (2) IOбуває монадою, що виявляється дуже зручним. Монади по суті не пов’язані з побічними ефектами. Слід також зазначити, що існує значення "ефекту", яке є ширшим, ніж "побічний ефект" у своєму звичному розумінні - таке, що включає чисті обчислення. Щодо цього останнього пункту, дивіться також, що саме означає «ефективний» .
дуплод

(До речі, @hammar, я взяв на себе свободу змінити "побічний ефект" на "ефект" у цій відповіді з причин, викладених у коментарі вище.)
duplode

17

Це приблизно як fmap, за винятком того, що ви можете запускати ефекти всередині функції mapper, яка також змінює тип результату.

Уявіть список цілих чисел , що представляють ідентифікатори користувачів в базі даних: [1, 2, 3]. Якщо ви хочете fmapдодати ці ідентифікатори користувачів до імен користувачів, ви не можете використовувати традиційні fmap, оскільки всередині функції вам потрібен доступ до бази даних для читання імен користувачів (що вимагає ефекту - в даному випадку за допомогою IOмонади).

Підпис traverse:

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)

Таким чином traverse, ви можете робити ефекти, отже, ваш код для зіставлення ідентифікаторів користувачів з іменами користувачів виглядає так:

mapUserIDsToUsernames :: (Num -> IO String) -> [Num] -> IO [String]
mapUserIDsToUsernames fn ids = traverse fn ids

Існує також функція, яка називається mapM:

mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)

Будь-яке використання mapMможна замінити на traverse, але не навпаки. mapMпрацює лише для монад, тоді як traverseє більш загальним.

Якщо ви просто хочете досягти ефекту і не повернути жодного корисного значення, існують traverse_і mapM_версії цих функцій, обидві з яких ігнорують повернене значення з функції і є дещо швидшими.



7

traverse це петля. Його реалізація залежить від структури даних, яку потрібно пройти. Це може бути список, дерево, Maybe, Seq(uence), або що - небудь , що має загальний спосіб перетинається через що - щось подібне для циклу або рекурсивної функції. Масив мав би цикл for, список - цикл while, дерево або щось рекурсивне, або комбінацію стека з циклом while; але у функціональних мовах ці громіздкі команди циклу вам не потрібні: ви поєднуєте внутрішню частину циклу (у формі функції) зі структурою даних більш безпосередньо і менш детально.

За допомогою Traversabletypeclass ви, напевно, можете написати свої алгоритми більш незалежними та універсальними. Але мій досвід говорить, що Traversableце зазвичай використовується лише для простого приклеювання алгоритмів до існуючих структур даних. Дуже приємно не потрібно писати подібні функції для різних типів даних, які також кваліфіковані.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.