【问题标题】:Are Functor instances unique?Functor 实例是唯一的吗?
【发布时间】:2013-11-15 11:53:38
【问题描述】:

我想知道 Haskell 中的 Functor 实例在多大程度上(唯一地)由函子定律确定。

由于ghc 至少可以为“普通”数据类型派生Functor 实例,因此它们似乎至少在各种情况下必须是唯一的。

为方便起见,Functor 定义和函子定律是:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

fmap id = id
fmap (g . h) = (fmap g) . (fmap h)

问题:

  • 可以从mapFunctor 实例data List a = Nil | Cons a (List a) 的假设出发推导出map 的定义吗?如果是这样,必须做出哪些假设才能做到这一点?

  • 是否有任何 Haskell 数据类型具有多个满足函子定律的 Functor 实例?

  • ghc 何时可以派生 functor 实例,何时不能派生?

  • 这一切都取决于我们如何定义平等吗? Functor 法则以值相等的形式表达,但我们不要求 Functors 具有 Eq 实例。那么这里有什么选择吗?

关于相等,当然有我称之为“构造函数相等”的概念,它允许我们推断[a,a,a] 对于任何类型的a 的任何值都与[a,a,a]“相等”,即使@987654339 @ 没有为它定义 (==)。所有其他(有用的)平等概念可能比这种等价关系更粗糙。但我怀疑Functor 法律中的平等更像是一种“推理平等”关系,并且可以是特定于应用程序的。对此有什么想法吗?

【问题讨论】:

  • Either a b 可以通过两种方式成为函子。 (a, b) 也可以...这些都是微不足道的例子,但我认为不会有一些非微不足道的例子。
  • @poorsod 不,它不能,使用类型柯里化实现它的唯一方法是将f 应用于Right 的值,否则noop
  • @jozefg 你是对的 - 我想这是 Haskell 类型类 Functor数学事物 'functor 之间的摩擦点'。

标签: haskell ghc functor


【解决方案1】:

请参阅 Brent Yorgey 的 Typeclassopedia

与我们将遇到的其他类型类不同,给定类型最多有一个有效的 Functor 实例。这can be proven 通过free theorem 用于fmap 的类型。事实上,GHC can automatically derive Functor 实例适用于许多数据类型。

【讨论】:

    【解决方案2】:

    “GHC 什么时候可以派生函子实例,什么时候不能派生?”

    当我们有故意的循环数据结构时。类型系统不允许我们表达我们强制循环的意图。 所以,ghc 可以派生一个实例,相似我们想要的,但不一样。


    循环数据结构 可能是唯一应该以不同方式实现 Functor 的情况。 但话又说回来,它具有相同的语义。

    data HalfEdge a = HalfEdge { label :: a , companion :: HalfEdge a }
    
    instance Functor HalfEdge where
        fmap f (HalfEdge a (HalfEdge b _)) = fix $ HalfEdge (f a) . HalfEdge (f b)
    

    编辑:

    HalfEdges 是表示图形中无向边的结构(在计算机图形学中称为 3d 网格...),您可以在其中引用任一端。通常它们会存储更多对相邻 HalfEdges、Nodes 和 Faces 的引用。

    newEdge :: a -> a -> HalfEdge a
    newEdge a b = fix $ HalfEdge a . HalfEdge b
    

    从语义上讲,没有fix $ HalfEdge 0 . HalfEdge 1 . HalfEdge 2,因为边总是由正好两个半边组成


    编辑 2:

    在 haskell 社区中,“Tying the Knot” 的引语以这种数据结构而闻名。它是关于语义上无限的数据结构,因为它们循环。它们只消耗有限的内存。示例:给定ones = 1:ones,我们将拥有twos 的这些语义等价实现:

    twos = fmap (+1) ones
    twos = fix ((+1)(head ones) :)
    

    如果我们遍历 twos 的(前 n 个元素)并且仍然引用该列表的开头,则这些实现在速度(每次计算 1+1 与仅计算一次)和内存消耗(O(n ) 与 O(1))。

    【讨论】:

    • 这不是函子。它违反了fmap id y = y,例如值y = fix $ HalfEdge 0 . HalfEdge 1 . HalfEdge 2。当然,只有一个正确的实例:fmap f (HalfEdge a rest) = HalfEdge (f a) $ fmap f rest。此版本还包括您的实现,这是一种特殊情况,仅适用于具有两个节点的圆形结构。
    • @JohannesGerer:该死!谢谢。是和不是。我应该澄清一下,这是一个实际的“HalfEdge”数据结构,而不是任何看起来像这样的结构。编辑...
    • 我知道您只是打算以这种方式使用它,但仍然:为什么不使用正确的仿函数实例,它与您的预期用例相同,并且适用于所有可能的用例?或者换一种说法,为什么“函子应该以不同的方式实现”?
    • @JohannesGerer “正确”仿函数实例?我猜你的意思是“派生”仿函数实例,它在语义上与我的实例相同,确实。派生的将是一个递归仿函数应用程序,它产生一个无限的HalfEdges列表......在内存中是无限的。相反,它应该是一个递归结构,只有两个项目相互指向。如果您没有发现差异,请阅读“Tying the Knot”(谷歌:haskell+Tying+the+Knot)。
    • 只要你只遵循有限数量的边,一切都很好。 PS:你不正确的函子实现当然也会产生一个无限的对象! PPS:如果是运行时差异(顺便说一句,这取决于编译器的实现),那么您应该在回答中这么说。在这种情况下,我仍然会建议另一种数据结构(它只保存两个实际值并为它们提供循环访问)并给它一个正确的仿函数实例。结论:如果你的类型允许不受支持的行为,那么你的程序就不是类型安全的,从而浪费了 Haskell 的最大优势之一!
    猜你喜欢
    • 2020-11-12
    • 2011-07-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-09
    • 1970-01-01
    • 1970-01-01
    • 2020-03-26
    相关资源
    最近更新 更多