如何解决如何在没有冗余方法的情况下在Purescript中定义类型类实例
我正在尝试正确地自学Purescript。我目前正在研究Purescript book,并正在学习第6章。由于我对Haskell非常熟悉,所以到目前为止,练习非常容易。但是我目前还停留在其中,我当然知道“怎么做”,但似乎因有关定义类型类实例的Purescript特定行为而失败。这使我对在实践中如何做到这一点感到非常好奇。
我要做的具体事情是为Foldable
类型定义一个NonEmpty
实例,其定义如下:
data NonEmpty a = NonEmpty a (Array a)
从Haskell的背景出发,我知道foldMap
往往是定义Foldable
实例的最简单方法,所以我没有花时间写:
instance foldableNonEmpty :: Foldable NonEmpty where
foldMap f (NonEmpty a as) = f a <> foldMap f as
这在Haskell中就足够了,因为所有其他Foldable
方法在foldMap
方面都有默认值。
我想在PureScript中也足够了,但是我的代码没有编译,给了我错误:
The following type class members have not been implemented:
foldr :: forall a b. (a -> b -> b) -> b -> ... -> b
foldl :: forall a b. (b -> a -> b) -> b -> ... -> b
in type class instance
Data.Foldable.Foldable NonEmpty
这让我感到惊讶,充其量似乎很不便。但是我检查了documentation,很快就发现确实有预定义的方法以foldl
和{{的形式,从foldr
中获取foldMap
和foldlDefault
1}}。所以我的下一个尝试是:
foldrDefault
但这也无法编译。这次的错误是:
instance foldableNonEmpty :: Foldable NonEmpty where
foldMap f (NonEmpty a as) = f a <> foldMap f as
foldr = foldrDefault
foldl = foldlDefault
这对我来说有点神秘,但是我认为这意味着我还不能访问 The value of foldableNonEmpty is undefined here,so this reference is not allowed.
和foldrDefault
,因为(根据Pursuit文档)它们已经需要类型构造函数了有一个foldlDefault
实例,因此不能用作定义该实例的一部分。
但是,所有这一切当然引出了一个问题:如何在Purescript中定义Foldable
实例,而不必手动为3种方法中的2种编写多余的定义?与其他彼此定义方法的类型类相似。或者,如果有可能(我希望如此!),我想念的是什么?
实际上,在搜索Google后,我确实找到了一个Foldable
实例的示例,并发现该实例确实可以编译:
Foldable
但是,这引出了一个新问题:据我所知,PureScript使用与Haskell完全相同的方式使用curring,为什么在上面的“ eta-reduced”版本中不允许这样做?以及与未定义值有关的错误消息与什么有关?
解决方法
是的,PureScript确实使用与Haskell完全相同的方式使用currying,但是在这里currying并不是问题。
问题是评估顺序。 Haskell使用正常的评估顺序,而PureScript使用可应用的评估顺序(简单来说,PureScript不是惰性的)。
这意味着Eta减少在PureScript和Haskell之间的工作方式不同(在某些情况下)。考虑以下示例:
f g x = if x > 0 then g x else 0
h x y = f (h x) y
如果我调用h 5 0
,则结果为0
,并且实际上从未递归调用h
,因为在f
内else
分支得到了求值。
在Haskell中,可以安全地减少Eta:
h x = f (h x)
但是,如果我在PureScript中编写了代码,则意味着对h x
的每次调用都必须立即递归地调用h x
才能将其结果传递给f
,从而导致无限递归
在Haskell中这是有效的,因为在传递给h x
之前,{em {em} 没有被评估,也就是“正常评估顺序”。
这与您的类实例类似,如果您还记得实例只是编译器会找出并为您透明传递的额外参数,并且实例声明可以看成是构造字典的函数(实际上就是这样)它已编译为JavaScript)。
要调用f
,必须将实例传递给它,但这意味着递归调用字典构造函数,这将导致无限递归。
但是如果您是Eta扩展的,则实例字典在其自身的构造过程中不是必需的,只有在实际使用参数调用foldrDefault
时才需要。
但是为什么世界上PureScript会使用可应用的评估顺序?!-您可能会愤慨地问。
好吧,我不是PureScript的设计师,所以我不能确切地回答这个问题,但是考虑到它已被编译为JavaScript,这对我来说确实有意义,并且使语义变得懒惰将意味着很多额外的麻烦。在编译后的代码中,这将使您难以在浏览器中进行检查和调试。
针对您的评论:
我在将其应用于实例定义时遇到了麻烦,特别是为什么为什么“如果您是Eta扩展的,则在其自身的构建过程中不需要实例字典”?仍然不需要该词典来确定foldrDefault到底是什么吗?
也许通过查看已编译的JavaScript会更容易说明。
对于减少Eta的情况,JavaScript如下所示:
foldr
对于以Eta扩展的情况,JavaScript如下:
var dictionary = {
...
foldr: foldrDefault(dictionary)
...
}
可以看到,在前一种情况下,var dictionary = {
...
foldr: function(y) { return foldrDefault(dictionary)(y) }
...
}
的值在dictionary
的初始化完成之前已传递给foldrDefault
。 JavaScript实际上允许这样做,并且运行时行为是dictionary
的参数最终将是foldrDefault
,这当然会导致运行时崩溃。
这实际上是一段时间以来的编译器错误(对不起,我现在找不到GitHub问题),补丁是简单地禁止这种模式,只允许Eta扩展的情况,在这种情况下,您会看到,undefined
的值只有在调用dictionary
时才传递给foldrDefault
,但不会在foldr
初始化期间传递。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。