Может ли кто-нибудь объяснить функцию перемещения в Haskell
Я новичок в Haskell. Я пытаюсь и не могу получить функцию traverse
от модуля Data.Traversable
. Я не вижу смысла. Поскольку я исхожу из императивного фона, может кто-нибудь, пожалуйста, объясните мне это с точки зрения императивной петли? Псевдокод был бы высоко оценен. Спасибо.
Ответы
Ответ 1
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
почти то же самое, за исключением того, что конструкторы вызываются в аппликативном стиле. Это означает, что мы можем иметь (побочные) эффекты при восстановлении дерева. Аппликация почти такая же, как монады, за исключением того, что эффекты не могут зависеть от предыдущих результатов. В этом примере это означает, что вы не могли бы сделать что-то другое с правой ветвью node в зависимости от результатов перестройки левой ветки, например.
Класс Traversable
также содержит монадическую версию mapM
, где эффекты могут в принципе зависеть от предыдущих результатов. (Хотя вы определенно не должны этого делать.) Например, выполните эффект f
дважды, если левая ветвь Empty
:
mapM f (Node l k r) = do
l' <- mapM f l
k' <- case l' of
Empty -> do _ <- f k; f k
_ -> f k
r' <- mapM f r
return $ Node l' k' r'
Если вы реализуете это на нечистом языке, fmap
и traverse
будут такими же, как mapM
, так как нет возможности предотвратить побочные эффекты. Вы не можете реализовать его как цикл, поскольку вам нужно рекурсивно перемещать структуру данных. Вот небольшой пример того, как я буду делать это в Javascript:
Node.prototype.traverse = function (f) {
return new Node(this.l.traverse(f), f(this.k), this.r.traverse(f));
}
Реализация этого как это ограничивает вас эффектами, которые позволяет язык. Если вы f.e. хотите не детерминировать (который экземпляр списка Аппликативных моделей) и ваш язык не имеют его встроенного, вам не повезло.
Ответ 2
traverse
превращает вещи внутри Traversable
в Traversable
вещей "внутри" a Applicative
, учитывая функцию, которая делает Applicative
вне вещей.
Используйте Maybe
как 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]]
Ответ 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
как об изменении порядка двух функторов, например. переходя из списка действий в действие, возвращающее список результатов.
So 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"]
Конечно, в этом случае он эквивалентен mapM
из Prelude. Это становится более интересным при использовании на других типах контейнеров или с использованием других аппликаций.
Ответ 4
Это похоже на 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 :: Monad m => (a -> m b) -> [a] -> m [b]
Любое использование mapM
можно заменить на traverse
, но не наоборот. mapM
обычно работает лучше, чем traverse
, но mapM
работает только для монадов со списками, тогда как traverse
является более общим.
Если вы просто хотите добиться побочного эффекта и не возвращать какое-либо полезное значение, существуют версии этих функций traverse_
и mapM_
, оба из которых игнорируют возвращаемое значение из функции и немного быстрее.
Ответ 5
traverse
- это цикл. Его реализация зависит от структуры данных, которая должна быть пройдена. Это может быть List, Tree, Maybe, Seq (ence) или что-нибудь, что имеет общий способ пройти через sth. как для петлевой или рекурсивной функции. У массива будет цикл for, цикл List a while, дерево или sth. рекурсивная или комбинация стека с циклом while; но в функциональных языках вам не нужны эти громоздкие петлевые команды: вы комбинируете внутреннюю часть цикла (в форме функции) с структурой данных более непосредственно и менее подробно.
С помощью Traversable typeclass вы, вероятно, можете записать свои алгоритмы более независимыми и универсальными. Но мой опыт говорит, что Traversable обычно используется только для простого склеивания алгоритмов с существующими структурами данных. Очень неплохо не писать аналогичные функции для разных типов данных, также квалифицированных.