Traversable typeclass的用途
有人能向我解释,类型类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
该应用程序的结果类型fa
必须是一个应用性,为什么呢? 一个仿函数是不够的?
Identity
是一个不好的例子,因为它总是只包含一个值。 你是对的 - 在这种情况下, Functor f
约束就足够了。 但很显然,大多数可穿越性在结构上并不是微不足道的。
所traverse
是:它以某种明确规定的顺序“访问”容器中的所有元素,对它们执行一些操作,并按原样重构结构。 这比任何一个都更强大
Functor t
,它还允许您访问/修改所有元素并重构结构,但只能完全相互独立(从而允许选择任意的计算顺序,在任何元素之前返回结构体一直(懒洋洋地)映射,等等)。 Foldable t
,它将元素按线性顺序排列,但不会重构结构。 基本上, Foldable
只是可以降级到一个简单列表的容器类,正如目击者所见
toList :: Foldable t => t a -> [a]
...或连接任何monoidal类型,通过
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
也是一个仿函数,所以可以将本地结果包装在任何数据构造函数中,从而不仅构建一个通用类列表,而且还构建一个任意的容器,包括一个具有原始结构的容器。
在Haskell中可Traversable
的方法将映射到容器上的概念(获得相似的容器作为回报)与“内部迭代器”的概念相结合,该概念为每个元素执行一个效果。
与外部迭代器相比,内部迭代器受到限制,因为我们不能使用从一个元素获得的值来决定如何处理其他元素。 我们不能说“嗯,如果某个元素的操作返回7,在处理下一个元素时发射导弹”。
这种类型的“刚性”计算,不能根据中途确定的值来改变路线,在Haskell中由Applicative
类型类型表示。 这就是Traversable
(容器)和Applicative
(效果)携手并进的原因。 Functor
是不够的,因为它没有提供一种结合有效行动的方法。
允许任何类型的Applicative
效果是一个福音; 这意味着我们可以遍历一个执行IO的容器,从失败早期开始,收集日志消息,从失败的迭代中收集错误消息,同时迭代......或者这些效果的任意组合。
为什么应用程序的结果类型必须是应用程序? 一个仿函数是不够的?
这是一个奇妙的问题。 最初的McBride&Paterson论文是朝另一个方向发展的:它注意到大量的计算在本质上是可应用的(可以用pure
和<*>
重写)。 然后它注意到某些容器,如[]
,允许这种类型的功能:
idist :: Applicative f => [f a] -> f [a]
idist = ...
我们现在在Traversable
类中调用sequence
。 一切都很好,但是当我们写抽象时,它有助于探究我们假设的强度。 如果我们试图在没有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
啊,更多的证据表明我们不需要应用程序的所有权力! 我们在这里只使用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
对于总和很有用,但是fmap
我们无法将两个不同的函数值粘合在一起。 只有通过<*>
我们才能在有限产品上“关闭”可穿越的容器。
这一切都很好,但缺乏精确度。 我们在这里可以找到证据表明Functor
可能不好,但是我们能否从第一个原则出发来论证Applicative
正是我们所需要的,不多也不少?
分类理论!
Jaskelioff&Rypacek论文的后半部分解决了这个问题。 在类别理论方面,如果允许一系列自然变换,函子T
是可穿越的
{ sequence | sequence : TFX -> FTX, any applicative F }
其中每个自然变换在F中是“自然的”,并且尊重“应用函子构成的monoidal结构”。 这是最后一句,这是最后一点行话,重要的是有Applicative
而不是Functor
。 使用Applicative f
,我们可以将fa
和fb
类型的值粘合在一起,在那里我们对它们进行操作(a foo <$> fa <*> fb
其中foo :: a -> b -> c
和fa, fb :: fa, fb
)或者将它们推到一个元组f (a, b)
。 这引起了前面提到的“monoidal结构”; 我们需要这个来证明遍历函子是关于有限产品的,就像我们上面所展示的那样。 没有应用程序,我们甚至无法开始讨论仿函数和产品如何相互作用! 如果Hask是我们的Haskell类型的类别,那么应用程序只是一种命名Hask- to- Hask endofunctors的方式,它在(->)
类型和产品类型周围“表现良好”。
希望这个双管齐下的答案,一个在实际编程中,另一个在分类foo-foo中,给出了一个直觉,为什么你在讨论可遍历性时想要应用函数。 我认为经常可以穿越他们周围的魔法元素,但他们的动机强烈的实际问题与坚实的理论基础。 其他语言生态系统可能有更容易使用的迭代模式和库,但我喜欢traverse
和sequence
的简单性和优雅性。