有人可以解释Haskell中的导线功能吗?


98

我正在尝试并且未能从取消该traverse功能Data.Traversable。我看不出它的意义。由于我来自命令式背景,因此有人可以通过命令式循环向我解释一下吗?伪代码将不胜感激。谢谢。


1
文章迭代器模式的本质,因为它建立横移一步一步的想法可能会有所帮助。一些先进的理念都存在,虽然
成龙

Answers:


120

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

像这样实现它会限制您使用该语言所允许的效果。如果您想要非确定性(Appliative模型的列表实例)并且您的语言没有内置的确定性,那么您很不幸。


11
“效应”一词是什么意思?
missingfaktor 2011年

23
@missingfaktor:表示a的结构信息Functor,该部分不是参数的。在状态值State中,失败MaybeEither,元件中的数量[]和,当然任意的外部副作用IO。我并不在意它是一个通用术语(就像Monoid使用“空”和“追加” 的功能一样,这个概念比起初的术语更通用),但它相当普遍并且足以满足目的。
CA McCann

@CA McCann:明白了。谢谢回答!
missingfaktor 2011年

1
“我很确定您不应该这样做[...]。” 绝对不是-就像ap依赖以前的结果那样令人讨厌。我已经相应地改写了这个话。
duplode

2
“适用性与单子几乎相同,除了效果不能取决于先前的结果。” ...很多东西用这行点击到位,谢谢!
agam

57

traverse打开里面的东西Traversable变成Traversable的东西“内部”的Applicative,给出一个函数,使Applicative出去了的东西。

让我们使用Maybeas Applicative并列出as 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]is [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]]

1
您的最终结果使我想起了这一点
hugomg 2011年

3
@missingno:是的,他们错过了fac n = length $ traverse rep [1..n]
Landei 2011年

1
实际上,它在“列表编码编程器”下(但使用列表推导)。该网站是全面的 :)
hugomg 2011年

1
@missingno:嗯,这不完全相同 ...两者都依赖于列表monad的笛卡尔乘积行为,但是该站点一次只使用两个,所以liftA2 (,)比起使用更通用的形式更像是这样做traverse
CA McCann

41

我认为这是最简单的方面来理解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

“排序副作用”是什么意思?您的答案中的“副作用”是什么-我只是认为副作用仅在monad中才有可能。问候。
Marek

1
@Marek“我只是认为副作用仅在monads中才有可能”-这种联系比这宽松得多:(1)该IO 类型可用于表达副作用;(2)IO碰巧是monad,事实证明非常方便。Monads基本上与副作用无关。还应该注意的是,在通常意义上,“效果”的含义比“副作用”的含义更广泛-其中包括纯计算。关于最后一点,另请参阅“有效”的确切含义
duplode '18

(顺便说一句,@ hammar,由于上述评论中列出的原因,我自由地在此答案中将“副作用”更改为“效应”。)
duplode

17

有点像fmap,不同之处在于您可以在mapper函数内部运行效果,这也会更改结果类型。

想象一下代表数据库中用户ID的整数列表:[1, 2, 3]。如果要将fmap这些用户ID用作用户名,则不能使用传统的fmap,因为您需要在函数内部访问数据库以读取用户名(这需要效果-在这种情况下,使用IOmonad)。

的签名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_这些功能,这两者忽略来自函数的返回值,并稍快的版本。



7

traverse 循环。它的实现取决于要遍历的数据结构。这可能是一个列表,树MaybeSeq(uence),或任何具有的通过像一个for循环或递归函数被走过的通用方式。一个数组将有一个for循环,一个while循环列表,一棵树或某种递归结构或堆栈与while循环的组合;但是在函数式语言中,您不需要这些繁琐的循环命令:您可以将循环的内部部分(以函数的形式)与数据结构以更直接的方式结合在一起,而不再那么冗长。

使用Traversable类型类,您可能可以编写更加独立和通用的算法。但是根据我的经验,这Traversable通常仅用于简单地将算法粘合到现有数据结构上。无需为合格的不同数据类型编写相似的函数也很不错。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.