Может кто-нибудь объяснить функцию перемещения в Хаскеле?
Я пытаюсь и не могу ухватить traverse
функция от Data.Traversable
, Я не могу понять его смысл. Так как я имею императивное происхождение, может кто-нибудь объяснить мне это с точки зрения императивного цикла? Псевдокод будет высоко ценится. Благодарю.
5 ответов
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 моделей), а в вашем языке нет встроенных функций, вам не повезло.
traverse
превращает вещи в Traversable
в Traversable
вещей "внутри" и Applicative
, учитывая функцию, которая делает Applicative
из вещей.
Давайте использовать Maybe
как Applicative
и перечислить как Traversable
, Сначала нам понадобится функция преобразования:
half x = if even x then Just (x `div` 2) else Nothing
Так что, если число четное, мы получаем половину его (внутри 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]]
Я думаю, что это проще всего понять с точки зрения 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
используется на других типах контейнеров, или с использованием других аппликативов.
Это вроде как 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_
версии этих функций, которые игнорируют возвращаемое значение из функции и немного быстрее.
traverse
это петля. Его реализация зависит от структуры данных, которую необходимо пройти. Это может быть список, дерево, Maybe
, Seq
(uence), или что-нибудь, что имеет общий способ обхода через что-то вроде цикла for или рекурсивной функции. Массив будет иметь цикл for, список цикла while, дерево либо что-то рекурсивное, либо комбинацию стека с циклом while; но в функциональных языках вам не нужны эти громоздкие команды цикла: вы комбинируете внутреннюю часть цикла (в форме функции) со структурой данных более прямым и менее многословным способом.
С Traversable
Класс типов, вы могли бы написать свои алгоритмы более независимыми и универсальными. Но мой опыт говорит, что Traversable
обычно используется только для простого склеивания алгоритмов с существующими структурами данных. Неплохо не писать одинаковые функции для разных типов данных.