【发布时间】:2011-11-19 14:55:34
【问题描述】:
我正在尝试从 Data.Traversable 获取 traverse 函数,但未能成功。我看不出它的意义。由于我来自命令式背景,有人可以用命令式循环向我解释一下吗?伪代码将不胜感激。谢谢。
【问题讨论】:
-
文章The Essence of the Iterator Pattern 可能会有所帮助,因为它逐步建立了遍历的概念。虽然存在一些高级概念
我正在尝试从 Data.Traversable 获取 traverse 函数,但未能成功。我看不出它的意义。由于我来自命令式背景,有人可以用命令式循环向我解释一下吗?伪代码将不胜感激。谢谢。
【问题讨论】:
我认为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 用于其他类型的容器或其他应用程序时,事情会变得更有趣。
【讨论】:
sequenceA . fmap for lists 就相当于sequence . map 不是吗?
IO type 可以是用于表示副作用; (2) IO 恰好是一个monad,结果非常方便。单子本质上与副作用无关。还应该注意的是,“效果”的含义比通常意义上的“副作用”更广泛——其中包括纯计算。关于最后一点,另请参阅What exactly does “effectful” mean。
traverse 与fmap 相同,除了它还允许您在重建数据结构时运行效果。
查看Data.Traversable 文档中的示例。
data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)
Tree 的 Functor 实例将是:
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 的一元版本,称为 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));
}
这样实现它会将您限制在语言允许的效果范围内。如果你 f.e.想要不确定性(Applicative 模型的列表实例)并且您的语言没有内置它,那么您就不走运了。
【讨论】:
Functor的结构信息,非参数部分。 State 中的状态值,Maybe 和 Either 中的失败,[] 中的元素数量,当然还有IO 中的任意外部副作用。我不关心它作为一个通用术语(就像 Monoid 函数使用“空”和“附加”,这个概念比这个术语最初建议的更通用)但它相当普遍并且很好地服务于目的。
ap 的效果依赖于先前的结果一样令人讨厌。我已经相应地改写了那句话。
traverse 将 Traversable 内部的事物转换为 Traversable 事物“内部”的事物 Applicative,给定一个使 Applicatives 脱离事物的函数。
让我们将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]]
【讨论】:
fac n = length $ traverse rep [1..n]
liftA2 (,),而不是使用traverse 的更通用的形式。
> transpose [[1],[2,2],[3,3,3]] 是 [[1,2,3],[2,3],[3]] 或 tails 和 > transpose [[1,2,3],[1,2,3],[1,2,3]] 是 [[1,1,1],[2,2,2],[3,3,3]]
traverse 是循环。它的实现取决于要遍历的数据结构。这可能是一个列表、树、Maybe、Seq(uence),或者任何具有通过 for 循环或递归函数之类的通用方式进行遍历的东西。一个数组将有一个 for 循环、一个列表、一个 while 循环、一个树,要么是递归的,要么是堆栈与 while 循环的组合;但在函数式语言中,您不需要这些繁琐的循环命令:您将循环的内部部分(以函数的形式)与数据结构以更直接的方式和更少的冗长方式结合起来。
使用Traversable 类型类,您可以编写更加独立和通用的算法。但我的经验表明,Traversable 通常仅用于简单地将算法粘合到现有数据结构上。也不需要为不同的数据类型编写类似的函数,这很好。
【讨论】:
有点像fmap,只是你可以在映射器函数中运行效果,这也会改变结果类型。
想象一个表示数据库中用户 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_版本,它们都忽略了函数的返回值,速度稍快。
【讨论】: