Может кто-нибудь объяснить функцию обхода в Haskell?

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));
}

Такая реализация ограничивает вас теми эффектами, которые позволяет язык. Если вам нужен недетерминизм (который является экземпляром списка моделей Applicative), а в вашем языке он не встроен, вам не повезло.

Сьерд Вишер
источник
11
Что означает термин «эффект»?
missingfaktor
24
@missingfaktor: Это означает структурную информацию о Functorчасти, которая не является параметрической. Значение состояния в State, сбой в Maybeи Either, количество элементов в [], и, конечно, произвольные внешние побочные эффекты в IO. Меня не интересует это как общий термин (например, Monoidфункции, использующие «empty» и «append», концепция более универсальна, чем этот термин сначала предполагает), но он довольно распространен и достаточно хорошо служит цели.
CA McCann
@CA McCann: Понятно. Спасибо за ответы!
missingfaktor
1
«Я почти уверен, что ты не должен этого делать [...]». Определенно нет - это было бы так же неприятно, как ставить эффекты в apзависимость от предыдущих результатов. Я соответствующим образом изменил это замечание.
duplode
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]]
Landei
источник
1
Ваш конечный результат мне об этом напоминает .
hugomg
3
@missingno: Да, они пропустилиfac n = length $ traverse rep [1..n]
Ландей
1
На самом деле, он находится в разделе «Программист кодирования списков» (но с использованием понимания списков). Этот веб-сайт исчерпывающий :)
hugomg
1
@missingno: Хм, это не совсем то же самое ... оба полагаются на декартово поведение продукта монады списка, но сайт использует только два за раз, так что это больше похоже на выполнение, liftA2 (,)чем на использование более общей формы traverse.
CA McCann
42

Я думаю, что это легче всего понять в терминах 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 08
Что вы имеете в виду под «секвенированием побочных эффектов»? Что такое «побочный эффект» в вашем ответе - я просто подумал, что побочные эффекты возможны только в монадах. С уважением.
Марек
1
@Marek «Я просто подумал, что побочные эффекты возможны только в монадах» - связь намного слабее, чем это: (1) IO Тип может использоваться для выражения побочных эффектов; (2) IOоказывается монадой, что оказывается очень удобным. Монады по существу не связаны с побочными эффектами. Также следует отметить, что есть значение «эффект», которое шире, чем «побочный эффект» в его обычном смысле, включающем чистые вычисления. По этому последнему пункту см. Также Что именно означает «эффективный» .
duplode
(Кстати, @hammar, я позволил себе изменить «побочный эффект» на «эффект» в этом ответе по причинам, изложенным в комментарии выше.)
duplode
17

Это вроде как fmap, за исключением того, что вы можете запускать эффекты внутри функции сопоставления, которая также меняет тип результата.

Представьте список целых чисел , представляющие идентификаторы пользователей в базе данных: [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; но в функциональных языках вам не нужны эти громоздкие команды цикла: вы объединяете внутреннюю часть цикла (в форме функции) со структурой данных более прямым и менее подробным образом.

С Traversableклассом типов вы, вероятно, могли бы написать свои алгоритмы более независимыми и универсальными. Но мой опыт говорит, что Traversableэто обычно используется только для простого приклеивания алгоритмов к существующим структурам данных. Довольно хорошо, что не нужно писать похожие функции для разных типов данных.

комонада
источник