【问题标题】:The purpose of the Traversable typeclassTraversable 类型类的目的
【发布时间】:2018-01-29 14:17:22
【问题描述】:

有人可以向我解释一下,类型类Traversable 的目的是什么?

类型类定义为:

class (Functor t, Foldable t) => Traversable (t :: * -> *) where

所以TraversableFunctor tFoldable 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的结果类型必须是应用程序,为什么?一个函子还不够吗?

【问题讨论】:

  • Functor 对于Identity 实例来说就足够了,但这是一个微不足道的实例。这对于几乎任何递归类型都是不够的 - 看看 Traversable [] 实例。
  • 非应用版调用Functor

标签: haskell


【解决方案1】:

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
    

    这里,对每个元素的运算结果通过幺半群运算组合(或者,如果没有元素,则结果为mempty)。

traverse 的情况下,Applicative f 约束基本上将这个幺半群组合提升到你也可以重建结构的东西。对应的是

mempty      ::   m
pure mempty :: f m

(<>)        ::   m ->   m ->   m
liftA2 (<>) :: f m -> f m -> f m

...但是另外,因为f 也是一个函子,所以您可以将本地结果包装在任何数据构造函数中,从而不仅可以构建一个通用的类似列表的东西,还可以构建一个任意容器,包括一个带有原始的容器结构。

【讨论】:

  • 能否请您澄清更多关于`traverse`和Applicative f的信息,我无法关注。
  • 是的……你有什么不明白的?
  • 关于traverseApplicative f的最后一节。
  • 好吧,如果没有Applicative,你既不能做pure mempty 也不能做liftA2 (&lt;&gt;),所以无法组合各个结果。
  • @klabe 按定义 以同样的方式。即traverse (pure . f) ≡ pure . fmap f.
【解决方案2】:

应用程序 f a 的结果类型必须是应用程序,为什么?一个函子还不够吗?

这是一个奇妙的问题。原始的McBride & Paterson 论文朝着另一个方向发展:它注意到许多计算本质上是可应用的(可以用pure&lt;*&gt; 重写)。 然后它注意到某些容器,比如[],允许这种类型的函数:

 idist :: Applicative f => [f a] -> f [a]
 idist = ...

我们现在在 Traversable 类中调用 sequence。一切都很好,但它有助于在我们编写抽象时探索我们假设的强度。如果我们尝试构建一个没有Applicative 的可遍历库,只使用Functor 会怎样?究竟会出什么问题?

产品!

为此,阅读Jaskelioff & Rypacek 论文会有所帮助,该论文试图确定范畴论中与应用函子和可遍历容器相对应的结构。可遍历容器最有趣的特性是它们在有限的sumsproducts封闭。这对于 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

在这里,用 just 函子编写定义是不可能的:fmap 非常适合求和,但我们无法将两个不同的函子值“粘合”在一起。只有使用&lt;*&gt;,我们才能“关闭”有限产品上的可遍历容器。

这一切都很好,但缺乏精确性。在这里,我们是一种挑剔的证据,证明 Functor 可能不好,但我们能否从第一原则论证 Applicative 正是我们所需要的,不多也不少?

范畴论!

Jaskelioff & Rypacek 论文的后半部分解决了这个问题。在范畴论术语中,函子T 是可遍历的,当且仅当它允许一系列自然变换时

{ sequence | sequence : TFX -> FTX, any applicative F }

每个自然变换都是“F 中的自然”并尊重“monoidal structure of applicative functor composition”。这是最后一个短语,最后一个小术语,重要的是拥有Applicative 而不是Functor。使用Applicative f,我们可以将f af b 类型的值粘合在一起,我们可以对它们进行操作(例如foo &lt;$&gt; fa &lt;*&gt; fb,其中foo :: a -&gt; b -&gt; cfa, fb :: f a, f b)或者只是将它们推入一个元组f (a, b)。这就产生了前面提到的“单面结构”;我们需要它来证明可遍历函子在有限乘积上是封闭的,就像我们上面展示的那样。如果没有应用程序,我们甚至无法开始谈论函子和产品如何交互!如果 Hask 是我们的 Haskell 类型类别,那么应用程序只是将 Hask 命名为“表现良好”的 Hask 内函子的一种方式围绕(-&gt;) 类型和产品类型。

希望这个两管齐下的答案,一个在实际编程中,一个在分类 foo-foo 中,可以让您直观地了解为什么在谈论可遍历性时需要应用函子。我认为通常在引入可遍历对象时带有一种魔法元素,但它们的动机很大程度上是出于对扎实理论基础的实际问题的关注。其他语言生态系统可能有更易于使用的迭代模式和库,但我喜欢 traversesequence 的简洁和优雅。

【讨论】:

    【解决方案3】:

    Haskell 中的Traversable 将映射到容器的概念(得到一个形状相似的容器作为回报)与为每个元素执行效果的"internal iterator" 的概念统一起来。

    与外部迭代器相比,内部迭代器受到限制,因为我们不能使用为一个元素获得的值来决定如何处理其他元素。我们不能说“嗯,如果某个元素的操作返回 7,则在处理下一个元素时发射导弹”。

    这种类型的“刚性”计算不能根据中途确定的值改变路线,在 Haskell 中由 Applicative 类型类表示。这就是Traversable(容器)和Applicative(效果)齐头并进的原因。 Functor 是不够的,因为它没有提供组合有效动作的方法。

    允许任何类型的Applicative 效果是一个福音;这意味着我们可以遍历执行 IO 的容器,existing early from failurecollecting log messagescollecting error messages from failed iterationsiterating concurrently... 或 any combination 的这些效果。

    【讨论】:

    • 我认为“与...相比”部分可能会被误解。当然,IO monad 允许检查 7,将其保存在一些 ref 中,然后在处理下一个元素时发射导弹。
    • @chi 但是读取引用的 IO 操作在某种意义上是“固定的”。它仅取决于当前元素。您不能选择来阅读参考文献,具体取决于之前的结果!
    • 我完全同意,但这很微妙。您没有对上一个 IO 操作返回的值的直接访问权限。您确实可以访问相同的引用,其中可能会选择保存先前返回的值。 “固定” IO 操作可以从(无条件地!)访问对之前所做工作的日志的引用开始,然后根据它选择是否访问其他引用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-06
    • 2011-02-13
    • 2019-10-20
    • 1970-01-01
    • 2017-10-26
    • 2021-04-27
    • 1970-01-01
    相关资源
    最近更新 更多