有人可以向我解释,类型Traversable
的目的是什么?
类型类定义是:
class (Functor t, Foldable t) => Traversable (t :: * -> *) where
所以Traversable
是Functor t
和Foldable t
。
traverse
函数是Traversable
的成员,并具有以下签名:
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
为什么结果必须包含在应用程序中?有什么意义呢?
我有以下示例:
module ExercisesTraversable where
import Test.QuickCheck (Arbitrary, arbitrary)
import Test.QuickCheck.Checkers (quickBatch, eq, (=-=), EqProp)
import Test.QuickCheck.Classes (traversable)
type TI = []
newtype IdentityT a = IdentityT a
deriving (Eq, Ord, Show)
instance Functor IdentityT where
fmap f (IdentityT a) = IdentityT (f a)
instance Foldable IdentityT where
foldMap f (IdentityT a) = f a
instance Traversable IdentityT where
traverse f (IdentityT a) = IdentityT <$> f a
instance Arbitrary a => Arbitrary (IdentityT a) where
arbitrary = do
a <- arbitrary
return (IdentityT a)
instance Eq a => EqProp (IdentityT a) where (=-=) = eq
main = do
let trigger = undefined :: TI (Int, Int, [Int])
quickBatch (traversable trigger)
我们来看看traverse
实现:
traverse f (IdentityT a) = IdentityT <$> f a
应用程序f a
的结果类型必须是一个应用程序,为什么?算子还不够吗?
答案 0 :(得分:16)
Identity
有点不好,因为它总是只包含一个值。你是对的 - 在这种情况下,Functor f
约束就足够了。但很明显,大多数可穿越者在结构上都不是那么微不足道。
traverse
的作用是:它以一些明确指定的顺序“访问”容器中的所有元素,对它们执行一些操作,并按原样重建结构。这比任何一个都强大
Functor t
,它还允许您访问/修改所有元素并重建结构,但只能完全相互独立(因此允许选择任意的计算顺序,将thunk返回到结构中)在任何元素被(懒惰地)映射之前,等等。)。 Foldable t
,它以线性顺序显示元素,但不重建结构。基本上,Foldable
只是可以降级为简单列表的容器类,如
toList :: Foldable t => t a -> [a]
...或任何幺半群类型的串联,通过
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
这里,通过monoid操作对每个元素的操作结果进行组合(或者,如果没有元素,则结果为mempty
)。
在traverse
的情况下,Applicative f
约束基本上将这种幺半群组合提升到可以重建结构的东西。通信是
mempty :: m
pure mempty :: f m
和
(<>) :: m -> m -> m
liftA2 (<>) :: f m -> f m -> f m
...但另外,因为f
也是一个仿函数,你可以将局部结果包装在任何数据构造函数中,因此不仅可以构建类似通用列表的东西,还可以构建一个任意容器,包括一个原始结构。
答案 1 :(得分:12)
应用程序的结果类型必须是一个应用程序,为什么?算子还不够吗?
这是梦幻般的问题。最初的McBride & Paterson论文向另一个方向发展:它注意到很多计算本质上是适用的(可以用pure
和<*>
重写)。 然后它注意到某些容器,如[]
,允许这种类型的功能:
idist :: Applicative f => [f a] -> f [a]
idist = ...
现在我们在sequence
课程中调用Traversable
。一切都很好,但是当我们编写抽象时,它有助于探究我们假设的强度。如果我们仅使用Applicative
尝试构建没有Functor
的可遍历库,该怎么办?究竟会出什么问题?
为此,有助于阅读试图在类别理论中确定与应用仿函数和可遍历容器相对应的结构的Jaskelioff & Rypacek论文。可遍历容器最有趣的属性是它们在有限总和和产品下关闭。这对于Haskell编程非常有用,可以使用总和和产品定义大量数据类型:
data WeirdSum a = ByList [a] | ByMaybe (Maybe a)
instance Traversable WeirdSum where
traverse a2fb (ByList as) =
ByList <$> traverse a2fb as
traverse a2fb (ByMaybe maybeA) =
ByMaybe <$> traverse a2fb maybeA
啊,更多证据表明我们不需要Applicative的全部力量!我们这里只使用fmap
。现在有限的产品:
data WeirdProduct a = WeirdProduct [a] (Maybe a)
instance Traversable WeirdProduct where
traverse a2fb (WeirdProduct as aMaybe) =
WeirdProduct <$> traverse a2fb as <*> traverse a2fb aMaybe
这里不可能用只是仿函数来编写一个定义:fmap
非常适合求和,但却无法将两个不同的函数值“粘合”在一起。只有<*>
,我们才能在有限的产品上“关闭”可遍历的容器。
这一切都很好,但缺乏精确性。我们在这里挑选证据Functor
可能不好,但我们是否可以从Applicative
正是我们所需要的,不多也不少的第一原则进行论证?
这个问题在Jaskelioff&amp;的下半部分得到解决。 Rypacek纸。在类别理论术语中,如果算子T
允许一系列自然变换,则它是可遍历的
{ sequence | sequence : TFX -> FTX, any applicative F }
其中每个自然变换在“F中是自然的”并且尊重“monoidal structure of applicative functor composition”。这是最后一个短语,最后一小段行话,重要的是Applicative
而不是Functor
。使用Applicative f
,我们可以将f a
和f b
类型的值粘合在一起,我们会对其进行操作(la foo <$> fa <*> fb
其中foo :: a -> b -> c
和{ {1}})或者只是将它们推入元组fa, fb :: f a, f b
。这产生了上述“幺半群结构”;我们需要这样才能证明可穿越仿函数是关于有限积的,就像我们在上面所展示的那样。如果没有应用程序,我们甚至无法开始讨论仿函数和产品如何交互!如果 Hask 是我们的Haskell类型类别,那么应用程序只是一种命名 Hask -to- Hask endofunctors“表现良好”的方法约f (a, b)
种类型和产品类型。
希望这个双管齐下的答案,一个在实际编程中,一个在分类foo-foo中,给出了一些关于为什么在讨论可穿越性时想要应用仿函数的直觉。我认为通常可穿越的东西是围绕着他们的魔法元素引入的,但是他们非常依赖于具有坚实理论基础的实际关注。其他语言生态系统可能具有更易于使用的迭代模式和库,但我喜欢(->)
和traverse
的简洁和优雅。
答案 2 :(得分:11)
Traversable
统一了映射在容器上的概念(获得一个类似形状的容器作为回报),其概念为"internal iterator",为每个元素执行效果。
与外部迭代器相比,内部迭代器受到约束,因为我们不能使用为一个元素获得的值来决定如何处理其他元素。我们不能说“mmmm,如果某个元素的操作返回7,则在处理下一个元素时启动导弹”。
这种类型的“刚性”计算,不能根据中间确定的值改变过程,在Haskell中由Applicative
类型类表示。这就是Traversable
(容器)和Applicative
(效果)齐头并进的原因。 Functor
是不够的,因为它没有提供组合有效行为的方法。
允许任何类型的Applicative
效果都是有益的;这意味着我们可以遍历执行IO,existing early from failure,collecting log messages,collecting error messages from failed iterations,iterating concurrently ...或any combination这些效果的容器。