在这个例子中是否能够避免UndecidableInstances?

huangapple go评论62阅读模式
英文:

Is it able to avoid UndecidableInstances in this example?

问题

我正在尝试实现一个二叉树作为一个类型类:

class BinaryTree bt where
    empty :: bt a -> Bool
    empty = isNothing . root
    emptyTree :: bt a
    branch :: a -> bt a -> bt a -> bt a 
    root :: bt a -> Maybe a
    lbranch :: bt a -> bt a 
    rbranch :: bt a -> bt a 

并且我想通过这个定义来说明二叉树是数据容器:

instance BinaryTree bt => Functor bt where
    fmap f bt = case root bt of
        Nothing -> emptyTree
        Just a -> branch (f a) (f <$> lbranch bt) (f <$> rbranch bt)

我遇到了以下错误:

BinaryTree/BinaryTree.hs:18:10: error:
    • The constraint ‘BinaryTree bt’
        is no smaller than the instance head ‘Functor bt’
      (Use UndecidableInstances to permit this)
    • In the instance declaration for ‘Functor bt’

使用 UndecidableInstances 可以使代码编译通过。但我在想,这是否是使用 UndecidableInstances 的适当情况?我已经阅读了类似于“何时使用UndecidableInstances是安全的?”的文章,但我真的不太明白。

更新 1:关于“为什么使用类型类”

这是探索的一部分,我试图通过Haskell代码来区分完全二叉树和完美二叉树与一般二叉树。我希望通过Haskell代码来描述它们的属性以合适的方式。(这些属性是:1)完全二叉树与列表一一对应,可以使用 bt2listlist2bt,以及 2)完美二叉树是两个相同深度完美二叉树的组合。)

具体来说,最终目标是声明一个完全二叉树的类型,当编译时拒绝不是完全二叉树而是二叉树的值。(对于完美二叉树也是如此。)

这个探索仍然没有放弃 😊,欢迎提出建议。非常感谢!

英文:

I am trying to implement binary tree as a typeclass:

class BinaryTree bt where
    empty :: bt a -&gt; Bool
    empty = isNothing . root
    emptyTree :: bt a
    branch :: a -&gt; bt a -&gt; bt a -&gt; bt a 
    root :: bt a -&gt; Maybe a
    lbranch :: bt a -&gt; bt a 
    rbranch :: bt a -&gt; bt a 

And I want to illustrate that binary trees are data containers using this definition:

instance BinaryTree bt =&gt; Functor bt where
    fmap f bt = case root bt of
        Nothing -&gt; emptyTree
        Just a -&gt; branch (f a) (f &lt;$&gt; lbranch bt) (f &lt;$&gt; rbranch bt)

I've got:

BinaryTree/BinaryTree.hs:18:10: error:
    • The constraint ‘BinaryTree bt’
        is no smaller than the instance head ‘Functor bt’
      (Use UndecidableInstances to permit this)
    • In the instance declaration for ‘Functor bt’

Using UndecidableInstances absolutely makes the code compile. But I am wondering that is this a proper case to use UndecidableInstances?
I've read about articles like "When is UndecidableInstances safe?", but I do not really get it.


Update 1: About "Why using typeclasses"

It is a part of an exploration, in which I try to distinct complete binary trees and perfect binary trees from general binary trees by Haskell code, in a proper way, by describing the property of them in Haskell code. (The properties are: 1) complete binary trees are one-to-one correspond to lists, bt2list and list2bt are available, and 2) perfect binary trees is the combine of two same-depth perfect binary trees. )

Specifically, the ultimate goal is to declare a type for complete binary trees, that refuses a value which is a binary tree but not a complete binary tree when compiling. (And also for perfect binary trees.)

The exploration is still not given up 😂 and suggestions are welcomed. Many thanks!

答案1

得分: 8

在不可判定的实例上

UndecidableInstances 是一个大多数情况下无害的扩展。如果你不得不启用它,不要担心。

在最坏的情况下,如果你碰巧有相互依赖的实例,像这样的情况下,UndecidableInstances 可以使编译器陷入无限循环:

instance A t =&gt; B t where ...
instance B t =&gt; A t where ...

如果你小心避免这种情况,编译器将正常工作。

这个扩展对编译后的代码没有不良影响,只对编译本身有影响。如果你的代码编译通过,那就是安全的。

在重叠实例上

另一方面,这个是需要担心的情况:

instance BinaryTree bt =&gt; Functor bt where

这个实例的头部是 Functor t,与任何其他 Functor 实例都重叠。这是要避免的。

Haskell 实例解析根本不进行回溯,因为这可能导致编译时间呈指数级增长,破坏分开编译。没有明智的方式来编写类似以下的代码:

instance MyClass1 f =&gt; Functor f where ...
instance MyClass2 f =&gt; Functor f where ...

为了使这个工作,当解析 Functor X 时,Haskell 应首先尝试解析 MyClass1 X,如果失败则尝试 MyClass2 X。这在实践中变得过于问题复杂。

请注意,Haskell 允许在任何模块中声明实例。所以要检查“没有 MyClass1 X 的实例”,Haskell 需要读取你程序中的所有模块。这将阻止编译器逐模块编译,因此是不允许的。

因此,像这样的实例:

instance MyClass1 f =&gt; Functor f where ...

实际上意味着“如果你正在解析 Functor X,你必须拥有 MyClass X。如果没有,就失败”。这不是一个蕴含,而更像是一个“如果且仅如果”。编译器会承诺选择其头部(Functor f 部分)匹配的第一个实例。直到编译器做出承诺之前,不会考虑实例上下文(MyClass1 f =&gt; 部分)。

有一些方式可以放宽对重叠实例的限制,但我不建议这样做。如果你不精确地理解底层发生了什么,它们会非常脆弱。

结论

为了避免重叠,你可以使用一个新类型包装器:

newtype F f a = F (f a)
instance BinaryTree bt =&gt; Functor (F bt) where

即使它不是等价的,你可以指定一个函子超类:

class Functor bt =&gt; BinaryTree bt where ...

这将强制新实例确保树是函子。

或许更重要的是,你还应该考虑完全不使用类型类来完成这个任务。在Haskell中,除非你有几个实例在考虑之中,否则声明类型类是相当不寻常的。在你的情况下,你的 class 似乎是一个单一的类型:二叉树。它的所有实例本质上都是相同的类型。那么,为什么要使用类呢?

英文:

On undecidable instances

UndecidableInstances is a mostly harmless extension. Don't worry if you have to turn it on.

In the worst case, UndecidableInstances can make the compiler get stuck in an infinite loop if you happen to have mutually depending instances, such as

instance A t =&gt; B t where ...
instance B t =&gt; A t where ...

If you are careful to avoid that, the compiler will just work fine.

The extension has no ill effects on the compiled code, only on the compilation itself. If your code compiles, you are safe.

On overlapping instances

On the other side, this is something to worry

instance BinaryTree bt =&gt; Functor bt where

The head of this instance is Functor t and that overlaps with any other instances for Functor. That is to be avoided.

Haskell instance resolution performs no backtracking at all, since that can lead to an exponential blow up in compilation time, and break separate compilation. There is no sane way to write something like

instance MyClass1 f =&gt; Functor f where ...
instance MyClass2 f =&gt; Functor f where ...

To make this work, when resolving Functor X, Haskell should first try to resolve MyClass1 X, and if that fails try MyClass2 X. This turns out to be too problematic in practice.

Note that Haskell allows instances to be declared in any module. So to check "no instance for MyClass1 X" Haskell would need to read all the modules in your program. This would prevent the compiler to compile a module at a time, so it's disallowed.

So, an instance like

instance MyClass1 f =&gt; Functor f where ...

actually means "If you are resolving Functor X, you must have MyClass X. If you don't, fail". It's not an implication but more like an "if and only if". The compiler commits to the first instance whose head (the Functor f part) matches. The instance context (the MyClass1 f =&gt; part) is not considered until the compiler has committed.

There are some ways to relax the constraints on overlapping instances, but I don't recommend those. They're very fragile if you don't precisely understand what's going on under the hood.

Conclusion

To avoid the overlap, you could use a newtype wrapper:

newtype F f a = F (f a)
instance BinaryTree bt =&gt; Functor (F bt) where

Even if it's not equivalent, you could specify a functor superclass:

class Functor bt =&gt; BinaryTree bt where ...

This will force new instances to ensure trees are functors.

Perhaps more importantly, you should also consider to avoid using type classes at all for this task. It's pretty unusual in Haskell to declare a type class unless you have several instances in mind. In your case, your class seems to be a single type: a binary tree. All its instances will be essentially the same type. So, why using classes at all?

huangapple
  • 本文由 发表于 2023年6月26日 18:29:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/76555825.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定