Haskell 函子实例是唯一的吗?

Haskell 函子实例是唯一的吗?,haskell,ghc,functor,Haskell,Ghc,Functor,我想知道Haskell中的函子实例在多大程度上是由函子定律决定的(唯一的) 由于ghc可以派生至少“一般”数据类型的Functor实例,因此它们至少在各种情况下必须是唯一的 为方便起见,函子的定义和函子定律如下: class Functor f where fmap :: (a -> b) -> f a -> f b fmap id = id fmap (g . h) = (fmap g) . (fmap h) 问题: 人们能否从假设数据列表a=Nil | Cons

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

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

为方便起见,
函子的定义和函子定律如下:

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

fmap id = id
fmap (g . h) = (fmap g) . (fmap h)
问题:

  • 人们能否从假设
    数据列表a=Nil | Cons a(列表a)
    函子开始推导
    映射的定义?如果是,为了做到这一点,必须做出哪些假设

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

  • 什么时候
    ghc
    可以派生出
    函子
    实例,什么时候不能

  • 所有这些都取决于我们如何定义平等吗?
    函子
    定律用值的相等表示,但我们不要求
    函子
    具有
    Eq
    实例。那么这里有选择吗

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

请参阅布伦特·约基的:

与我们将遇到的其他类型类不同,给定类型最多有一个有效的函子实例。这通过fmap类型的。事实上,函子是许多数据类型的实例

“GHC何时能派生函子实例,何时不能?”

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


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

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)

编辑:

半边是表示图形中无向边的结构(在计算机图形学、3d网格等中已知),可以在其中引用任意一端。通常,它们会存储对相邻半边、节点和面的更多引用

newEdge :: a -> a -> HalfEdge a
newEdge a b = fix $ HalfEdge a . HalfEdge b
语义上,没有
fix$HalfEdge 0。半边旗1。半边2
,因为边总是由两条半边组成


编辑2:

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

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

如果我们遍历(前n个元素)
two,并且仍然有对该列表开头的引用,这些实现在速度(每次计算1+1而不是仅计算一次)和内存消耗(O(n)而不是O(1))上都有所不同。

或者b
可以以两种方式作为函子。所以可以
(a,b)
。。。这些都是琐碎的例子,但我认为不可能有一些非琐碎的例子。@poorsod不,不可能,对于类型curry,实现它的唯一方法是将
f
应用于
Right
的值,否则noop@jozefg你说得对-我想这是Haskell typeclass
函子和数学“函子”之间的摩擦点。另请参见。这不是函子。它违反了
fmap id y=y
,例如值
y=fix$HalfEdge 0。半边旗1。半边旗2
。当然,只有一个正确的实例:
fmap f(半边a rest)=半边(fa)$fmap f rest
。此版本还包括您的实现,这是一个特例,仅适用于具有两个节点的循环结构。@JohannesGerer:dang!谢谢。是和否。我应该澄清一下,这是一个实际的“半边”数据结构,而不是任何看起来像这样的结构。编辑…我知道您只打算这样使用它,但是:为什么不使用正确的functor实例,它与您的预期用例相同,并且对所有可能的用例都正确?或者换一种说法,为什么“函子应该以不同的方式实现”?@JohannesGerer“correct”函子实例?我猜你指的是“派生”函子实例,它的语义实际上与我的实例相同。派生的将是一个递归函子应用程序,它生成无限多个半边列表。。。无限的记忆。相反,它应该是一个递归结构,只有两个项目相互指向。如果你看不出有什么不同,那就读一读“打结”(谷歌:haskell+打结+打结)。只要你只遵循有限的边,一切都很好。PS:当然,你不正确的函子实现也会产生一个无限对象!PPS:如果是运行时差异(顺便说一句,这取决于编译器实现),那么您应该在回答中这样说。在本例中,我仍然建议使用另一种数据结构(只包含两个实数并提供对它们的循环访问),并为其提供一个正确的函子实例。结论:如果您的类型允许不受支持的行为,从而浪费了Haskell最大的优势之一,那么您的程序就是不安全的!