【问题标题】:How to define an typeclass instance in Purescript without redundant methods如何在没有冗余方法的 Purescript 中定义类型类实例
【发布时间】:2020-10-13 20:09:09
【问题描述】:

我正在尝试正确地自学 Purescript。我目前正在阅读Purescript book 并在第 6 章。由于我对 Haskell 非常熟悉,所以到目前为止练习非常简单。但我目前坚持一个,我当然知道“如何”做,但似乎被关于定义类型类实例的 Purescript 特定行为所击败。这让我非常好奇这是如何在实践中完成的。

我正在尝试做的具体事情是为NonEmpty 类型定义一个Foldable 实例,其定义如下:

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,很快发现确实有预定义的方法可以从foldMap 获取foldlfoldr,形式为foldlDefaultfoldrDefault。所以我的下一个尝试是:

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.

这对我来说有点神秘,但我认为这意味着我还不能访问 foldrDefaultfoldlDefault,因为(根据 Pursuit 文档)它们要求类型构造函数已经有一个 @987654341 @instance,因此不能用作定义实例的一部分。

但这一切当然引出了一个问题:如何在 Purescript 中定义 Foldable 实例,而不必手动为 3 种方法中的 2 种写出冗余定义?与其他根据彼此定义方法的类型类类似。或者如果有可能(我希望如此!),我错过了什么?

其实,在谷歌搜索了一下之后,我确实找到了一个Foldable 实例的示例,发现如果我这样做,该实例确实可以编译:

instance foldableNonEmpty :: Foldable NonEmpty where
    foldMap f (NonEmpty a as) = f a <> foldMap f as
    foldr f = foldrDefault f
    foldl f = foldlDefault f

但这引出了一个新问题:据我所知,由于 PureScript 使用与 Haskell 完全相同的方式使用柯里化,为什么允许这样做而上面的“eta-reduced”版本却不允许?关于未定义值的错误消息与什么有什么关系?

【问题讨论】:

    标签: typeclass purescript


    【解决方案1】:

    是的,PureScript 确实使用与 Haskell 完全相同的方式使用柯里化,但柯里化不是这里的问题。

    问题是评估顺序。 Haskell 使用正常的求值顺序,而 PureScript 使用 applicative(简单来说——PureScript 不是惰性的)。

    这意味着 Eta-reduction 在 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 在传递到 f 之前没有评估 - 也就是“正常评估顺序”。


    它与您的类实例类似,如果您记得实例只是编译器计算并透明地为您传递的额外参数,并且实例声明可以看作是构造字典的函数(这确实是它已编译为 JavaScript)。

    要调用foldrDefault,必须将实例传递给它,但这意味着递归调用字典构造函数,这将导致无限递归。

    但如果您进行 Eta-expand,则实例字典在其自身构造期间不需要,但只有在实际使用参数调用 foldr 时才需要。


    但是为什么 PureScript 会使用应用评估顺序呢?! - 你可能会愤愤不平地问。

    好吧,我不是 PureScript 的设计者,所以我不能确切地回答这个问题,但考虑到它被编译为 JavaScript,并且使语义变得懒惰意味着要跳过很多额外的障碍,这对我来说确实有意义在编译后的代码中,在浏览器中检查和调试会让人头疼。


    回应您的评论:

    我在将其应用于实例定义时遇到了麻烦,特别是为什么“如果您进行 Eta 扩展,则在其自身构造过程中不需要实例字典”?为了弄清楚 foldrDefault 实际上是什么,是否仍然不需要该字典?

    也许通过查看已编译的 JavaScript 来说明会更容易。

    对于 Eta-reduced 的情况,JavaScript 看起来像这样:

    var dictionary = {
        ...
        foldr: foldrDefault(dictionary)
        ...
    }
    

    对于 Eta 扩展的情况,JavaScript 如下:

    var dictionary = {
        ...
        foldr: function(y) { return foldrDefault(dictionary)(y) }
        ...
    }
    

    可以看到,在前一种情况下,dictionary 的值在dictionary 的初始化完成之前传递给foldrDefault。 JavaScript 实际上允许这样做,运行时行为是foldrDefault 的参数最终会变成undefined,这当然会导致运行时崩溃。

    这实际上是编译器中的一个错误(我现在找不到 GitHub 问题,抱歉),补丁只是简单地禁止这种模式并只允许 Eta 扩展的情况,其中,如你可以看到,dictionary 的值被传递给foldrDefault 只有在foldr 被调用,而不是在dictionary 的初始化期间。

    【讨论】:

    • 谢谢 - 我知道 PureScript 是严格的,而 Haskell 是懒惰的(并且理解其中的原因),但还没有想到这会影响像 eta-reduction 这样简单的东西。你回答的那部分很有意义。但是,我在将其应用于实例定义时遇到了麻烦,特别是为什么“如果您进行 Eta 扩展,则在其自身构造过程中不需要实例字典”?为了弄清楚foldrDefault 实际上是什么,是否仍然不需要该字典?
    • @FyodorSoikin 这真是一个了不起的答案。我学到了很多东西。感谢您的深入研究!
    • 感谢@paluh 的客气话。这对我来说意义重大。
    • 老实说,我可能应该在你的几乎每一个答案@FyodorSoikin 下写这样的评论 :-) 我认为我们很幸运你是我们小型 PS 社区的一员。
    猜你喜欢
    • 2015-12-09
    • 1970-01-01
    • 2013-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多