英文:
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)完全二叉树与列表一一对应,可以使用 bt2list
和 list2bt
,以及 2)完美二叉树是两个相同深度完美二叉树的组合。)
具体来说,最终目标是声明一个完全二叉树的类型,当编译时拒绝不是完全二叉树而是二叉树的值。(对于完美二叉树也是如此。)
这个探索仍然没有放弃 😊,欢迎提出建议。非常感谢!
英文:
I am trying to implement binary tree as a typeclass:
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
And I want to illustrate that binary trees are data containers using this definition:
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)
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 => B t where ...
instance B t => A t where ...
如果你小心避免这种情况,编译器将正常工作。
这个扩展对编译后的代码没有不良影响,只对编译本身有影响。如果你的代码编译通过,那就是安全的。
在重叠实例上
另一方面,这个是需要担心的情况:
instance BinaryTree bt => Functor bt where
这个实例的头部是 Functor t
,与任何其他 Functor
实例都重叠。这是要避免的。
Haskell 实例解析根本不进行回溯,因为这可能导致编译时间呈指数级增长,破坏分开编译。没有明智的方式来编写类似以下的代码:
instance MyClass1 f => Functor f where ...
instance MyClass2 f => Functor f where ...
为了使这个工作,当解析 Functor X
时,Haskell 应首先尝试解析 MyClass1 X
,如果失败则尝试 MyClass2 X
。这在实践中变得过于问题复杂。
请注意,Haskell 允许在任何模块中声明实例。所以要检查“没有 MyClass1 X
的实例”,Haskell 需要读取你程序中的所有模块。这将阻止编译器逐模块编译,因此是不允许的。
因此,像这样的实例:
instance MyClass1 f => Functor f where ...
实际上意味着“如果你正在解析 Functor X
,你必须拥有 MyClass X
。如果没有,就失败”。这不是一个蕴含,而更像是一个“如果且仅如果”。编译器会承诺选择其头部(Functor f
部分)匹配的第一个实例。直到编译器做出承诺之前,不会考虑实例上下文(MyClass1 f =>
部分)。
有一些方式可以放宽对重叠实例的限制,但我不建议这样做。如果你不精确地理解底层发生了什么,它们会非常脆弱。
结论
为了避免重叠,你可以使用一个新类型包装器:
newtype F f a = F (f a)
instance BinaryTree bt => Functor (F bt) where
即使它不是等价的,你可以指定一个函子超类:
class Functor bt => 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 => B t where ...
instance B t => 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 => 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 => Functor f where ...
instance MyClass2 f => 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 => 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 =>
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 => Functor (F bt) where
Even if it's not equivalent, you could specify a functor superclass:
class Functor bt => 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?
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论