it-swarm.cn

有人可以解释Haskell中的遍历函数吗?

我正在尝试并且未能从Data.Traversable中查看traverse函数。我无法理解其观点。由于我来自一个势在必行的背景,有人可以根据命令性循环向我解释一下吗?伪代码将非常感激。谢谢。

88
Konan

traversefmap相同,只是它还允许您在重建数据结构时运行效果。

看一下Data.Traversable文档中的示例。

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

FunctorTree实例将是:

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实例几乎相同,只是构造函数以applicative样式调用。这意味着我们可以在重建树时获得(侧面)效果。应用与monad几乎相同,只是效果不能取决于之前的结果。在此示例中,这意味着您无法对节点的右侧分支执行不同的操作,具体取决于重建左侧分支的结果。

由于历史原因,Traversable类还包含名为traverse的monadic版本mapM。对于所有意图和目的mapMtraverse相同 - 它作为单独的方法存在,因为Applicative后来才成为Monad的超类。

如果您使用不纯的语言实现它,fmap将与traverse相同,因为无法防止副作用。您不能将其实现为循环,因为您必须递归遍历数据结构。这是一个小例子,我将如何在Javascript中执行此操作:

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

像这样实现它会限制您使用语言允许的效果。如果你f.e.想要非确定性(应用模型的列表实例)和你的语言没有内置,你运气不好。

105
Sjoerd Visscher

traverseTraversable中的内容转换为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]的结果是什么?

我们使用rep获得了[1][2,2][3,3,3]的部分结果。现在列表的语义为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]]
52
Landei

我认为用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 ()

所以你可以看到FoldableTraversable之间的关键区别在于后者允许你保留结构的形状,而前者要求你将结果折叠成其他值。


其用法的一个简单示例是使用列表作为可遍历结构,并使用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用于其他类型的容器或使用其他应用程序时,事情变得更有趣。

41
hammar

它有点像fmap,除了你可以在mapper函数中运行效果,它也会改变结果类型。

想象一下表示数据库中用户ID的整数列表:[1, 2, 3]。如果你想fmap这些用户ID到用户名,你不能使用传统的fmap,因为在函数内部你需要访问数据库来读取用户名(这需要一个效果 - 在这种情况下,使用IO monad) 。

traverse的签名是:

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

使用traverse,您可以执行效果,因此,将用户ID映射到用户名的代码如下所示:

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仅适用于monad,而traverse更通用。

如果你只是想实现一个效果并且不返回任何有用的值,那么这些函数的traverse_mapM_版本都会忽略函数的返回值,并且稍快一些。

16
Kai Sellgren

traverse循环。它的实现取决于要遍历的数据结构。这可能是一个列表,树,MaybeSeq(uence),或者具有通过类似for循环或递归函数遍历的通用方式的任何东西。数组将具有for循环,列表具有while循环,树可以是递归的,或者是堆栈与while循环的组合;但在函数式语言中,您不需要这些繁琐的循环命令:将循环的内部部分(以函数的形状)与数据结构以更直接的方式组合,并且更简洁。

使用Traversable类型类,您可以编写更加独立且通用的算法。但我的经验表明,Traversable通常仅用于将算法简化为现有数据结构。很高兴不需要为不同的数据类型编写类似的函数。

7
comonad