【问题标题】:How do I write, "if typeclass a, then a is also an instance of b by this definition."我该怎么写,“如果类型类 a,那么根据这个定义, a 也是 b 的一个实例。”
【发布时间】:2010-07-09 14:19:02
【问题描述】:

我有一个类型类MyClass,其中有一个函数可以生成String。我想用它来暗示Show 的一个实例,这样我就可以将实现MyClass 的类型传递给show。目前为止,

class MyClass a where
    someFunc :: a -> a
    myShow :: a -> String 

instance MyClass a => Show a where
    show a = myShow a

这给出了错误Constraint is no smaller than the instance head. 我也试过了,

class MyClass a where
    someFunc :: a -> a
    myShow :: a -> String

instance Show (MyClass a) where
    show a = myShow a

这给出了错误,ClassMyClass' used as a type`。

如何在 Haskell 中正确表达这种关系? 谢谢。

我应该补充一点,我希望跟进 MyClass 的特定实例,这些实例会根据它们的类型发出特定的字符串。例如,

data Foo = Foo
data Bar = Bar

instance MyClass Foo where
    myShow a = "foo"

instance MyClass Bar where
    myShow a = "bar"

main = do
    print Foo
    print Bar

【问题讨论】:

  • 仅供参考,我推荐了这个问题,因为它详细而简洁、清晰且格式良好。一个典型的问题,如果可能的话,我会再次推荐它。

标签: haskell


【解决方案1】:

我强烈反对迄今为止提出的不完善的解决方案。

instance MyClass a => Show a where
    show a = myShow a

由于实例解析的工作方式,这是一个非常危险的实例!

实例解析通过在每个实例的=> 右侧进行有效的模式匹配进行,完全不考虑=> 左侧的内容。

当这些实例都没有重叠时,这是一件美好的事情。但是,您在这里所说的是“这是您应该为 EVERY Show 实例使用的规则。当被问及任何类型的 show 实例时,您将需要 MyClass 的实例,所以去获取那个,这就是实现。” -- 一旦编译器承诺选择使用你的实例,(仅仅凭借'a' 与一切统一的事实)它就没有机会回退并使用任何其他实例!

如果你打开{-# LANGUAGE OverlappingInstances, IncoherentInstances #-}等使其编译,当你去编写模块时,你会得到不那么微妙的失败,这些模块导入提供这个定义的模块并且需要使用任何其他的Show实例。最终,您将能够使用足够的扩展来编译此代码,但遗憾的是它不会做您认为应该做的事情!

如果你考虑一下:

instance MyClass a => Show a where
    show = myShow

instance HisClass a => Show a where
    show = hisShow

编译器应该选择哪个?

您的模块可能只定义其中一个,但最终用户代码将导入一堆模块,而不仅仅是您的。另外,如果另一个模块定义了

instance Show HisDataTypeThatHasNeverHeardOfMyClass

编译器完全有权忽略他的实例并尝试使用您的实例。

遗憾的是,正确的答案是做两件事。

对于 MyClass 的每个 individual 实例,您可以使用非常机械的定义定义相应的 Show 实例

instance MyClass Foo where ...

instance Show Foo where
    show = myShow

这很不幸,但在只考虑少数 MyClass 实例时效果很好。

当您有大量实例时,避免代码重复的方法(当类比显示复杂得多时)是定义。

newtype WrappedMyClass a = WrapMyClass { unwrapMyClass :: a }

instance MyClass a => Show (WrappedMyClass a) where
    show (WrapMyClass a) = myShow a

这提供了新类型作为实例调度的载体。然后

instance Foo a => Show (WrappedFoo a) where ...
instance Bar a => Show (WrappedBar a) where ...

是明确的,因为 WrappedFoo aWrappedBar a 的类型“模式”是不相交的。

base 包中有许多这种习惯用法的例子。

出于这个原因,在 Control.Applicative 中有 WrappedMonadWrappedArrow 的定义。

理想情况下,您可以说:

instance Monad t => Applicative t where
    pure = return
    (<*>) = ap 

但实际上这个实例的意思是每个 Applicative 都应该通过首先为 Monad 找到一个实例,然后分派给它来派生。因此,虽然它的意图是说每个 Monad 都是 Applicative(顺便说一下,=&gt; 的含义是这样的),但它实际上说的是每个 Applicative 都是 Monad,因为有一个实例头 't' 匹配任何类型.在许多方面,'instance' 和 'class' 定义的语法是倒退的。

【讨论】:

  • 哈,我忙于解释错误消息以及 UndecidableInstances 的作用,以至于我完全忘记了放入我真正的解决方案(除了非常小的评论)!我已经编辑了我的回复以包含它。
  • 谢谢!正如我确定您可以从我的问题中看出的那样,我仍在学习究竟什么是“类型类”,这非常有帮助。尽管我不喜欢“设计模式”,但很高兴能看到一本详细介绍 Haskell 程序中常见情况的食谱解决方案的书,包括您所说的上述“成语”。
  • 顺便说一句,“一个类型类不能(安全地)自动派生(推断)另一个类型类,它只能要求(受其约束)一个”,这样说安全吗?
  • 嗯,偶尔(如果很少的话)批发地推断另一个类型类很有用,但只有 i 是另一个类型类的唯一目的。即你可以制作类 (Foo a, Bar a) => FooBar a;和实例 (Foo a, Bar a) => FooBar a 事实证明这是完全明确的,您已经为 Foo 和 Bar 的组合提供了别名。 (您需要一些扩展才能使其工作)但是是的,由于上述问题,一般不应使用类型类来推断其他类。
【解决方案2】:

(编辑:将正文留给后代,但跳到最后以获得真正的解决方案)

在声明instance MyClass a =&gt; Show a 中,让我们检查一下错误“约束不小于实例头”。约束是 '=>' 左侧的类型类约束,在本例中为 MyClass a。 “实例头”是您为其编写实例的类之后的所有内容,在本例中为a(在Show 的右侧)。 GHC 中的类型推断规则之一要求约束具有比头部更少的构造函数和变量。这是所谓的“Paterson Conditions”的一部分。这些是作为类型检查终止的保证。

在这种情况下,约束与头部完全相同,即a,因此它未通过此测试。您可以通过启用UndecidableInstances 来移除Paterson 条件检查,最有可能使用{-# LANGUAGE UndecidableInstances #-} pragma。

在这种情况下,您实际上是在使用您的类MyClass 作为Show 类的类型类同义词。像这样创建类同义词是 UndecidableInstances 扩展的规范用途之一,因此您可以在这里安全地使用它。

'Undecidable' 意味着 GHC 不能证明类型检查会终止。虽然听起来很危险,但启用 UndecidableInstances 可能发生的最坏情况是编译器将循环,最终在耗尽堆栈后终止。如果它编译,那么显然类型检查终止,所以没有问题。危险的扩展是 IncoherentInstances,这听起来很糟糕。

编辑:这种方法可能导致的另一个问题来自这种情况:

instance MyClass a => Show a where

data MyFoo = MyFoo ... deriving (Show)

instance MyClass MyFoo where

现在MyFoo 有两个 Show 实例,一个来自派生子句,一个用于 MyClass 实例。编译器无法决定使用哪个,因此它会以错误消息退出。如果您尝试创建您无法控制的类型的 MyClass 实例,这些类型已经具有 Show 实例,则必须使用 newtypes 来隐藏已经存在的 Show 实例。即使没有MyClass 实例的类型仍然会发生冲突,因为定义instance MyClass =&gt; Show a 因为该定义实际上提供了所有可能的a 的实现(稍后会进行上下文检查;它不涉及实例选择)

这就是错误消息以及 UndecidableInstances 如何使其消失。不幸的是,由于 Edward Kmett 解释的原因,在实际代码中使用会带来很多麻烦。最初的动力是避免在已经存在 MyClass 约束时指定 Show 约束。鉴于此,我要做的就是使用来自MyClassmyShow 而不是show。您根本不需要Show 约束。

【讨论】:

  • 谢谢,如果我启用 UndecidableInstances,它确实有效。奇怪的。这是对类型类系统的滥用吗?我主要是想避免必须为所有采用“MyClass”并且还需要显示的函数指定约束“显示”。由于所有 MyClass 实例都应该有一个与之关联的字符串,因此我认为自动派生 Show 将是一种方法。但是,通过在必要时显式指定 Show 来避免该问题似乎更好,而不是启用深奥的语言扩展。你怎么看?
  • 我会在所有函数中使用 myShow 而不是 show ,因此您根本不需要 Show 约束。如果合适的话,您可以在实例定义中编写myShow = show。由于编辑中描述的问题,我会避免写instance MyClass a =&gt; Show a。 Dave Hinton 的解决方案比 UndecidableInstances 更好,但我认为您不应该仅仅为了方便而添加超类约束。
  • 请记住,这种带有这些扩展的 hack 只能在单个模块中合理安全地工作。即使您不导出 MyClass,您甚至不能使用此模块的导出,在任何其他模块中,该模块将有一个传递依赖于它的模块,该模块想要显示任何不是 MyClass 实例的内容。
【解决方案3】:

我认为反过来做会更好:

class Show a => MyClass a where
    someFunc :: a -> a

myShow :: MyClass a => a -> String
myShow = show

【讨论】:

  • ...除了,这不会让 OP 声明自定义 show 版本。此外,它不允许为(还)不是 Show 实例的类型声明 MyClass
  • 这里要意识到的是,当你说 show = myShow 时,你已经输掉了自定义显示的战斗,因为一个类型不能有两个不同的显示功能,而一个类型implements MyClass 必须是 Show 的实例(将其称为 myShow 代替 show 没有区别。)也就是说,如果我想让 T 成为 MyClass 的实例,我必须实现 myShow,最后变成它的 show 函数作为 Show 的一个实例。将其与先实现 Show,然后再实现 MyClass 进行比较,您会发现唯一的区别是函数的名称。
  • 我喜欢这个解决方案,因为 OP 提出他的问题的方式,myShow 等同于 show 对于所有 MyClass 实例,因为 MyClass 将暗示 Show(根据 OP 的愿望) .不过,在这种情况下,您不需要 myShow 函数。只需使用show
【解决方案4】:

你可以编译它,但不能用 Haskell 98,你必须启用一些语言扩展:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
-- at the top of your file

灵活的实例允许在实例声明中使用上下文。我真的不知道 UndecidableInstances 的含义,但我会尽量避免。

【讨论】:

  • 我假设 GHC 认为它无法确定的事实可能是一个问题。如果有人能解释为什么它是不可判定的那就太好了!
  • 如果开启UndecidableInstances,FlexibleInstances就多余了。
【解决方案5】:

正如 Ed Kmett 指出的那样,这对于您的情况来说根本不可能。但是,如果您有权访问要为其提供默认实例的类,则可以使用默认实现将样板文件减少到最低限度,并使用您需要的默认签名约束输入类型:

{-# LANGUAGE DefaultSignatures #-}

class MyClass a where
    someFunc :: a -> Int

class MyShow a where
    myShow :: a -> String
    default myShow :: MyClass a => a -> String
    myShow = show . someFunc

instance MyClass Int where
    someFunc i = i

instance MyShow Int

main = putStrLn (myShow 5)

请注意,唯一真正的样板文件(好吧,除了整个示例)减少到 instance MyShow Int

查看aesons ToJSON 了解更实际的示例。

【讨论】:

    【解决方案6】:

    您可能会在相关的 SO 问题中找到一些有趣的答案:Linking/Combining Type Classes in Haskell

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-13
      相关资源
      最近更新 更多