使用可用的 class 个实例解决类型歧义

Resolving type ambiguities using available class instances

给定以下代码:

import Data.Word

data T = T deriving (Eq, Show)

class    C a      where f :: a -> ()
instance C T      where f _ = ()
instance C Word16 where f _ = ()

main = return $ f 0x16

GHC 抱怨说它无法推断文字 0x16 的类型应该是什么,错误是:

No instance for (Num a0) arising from the literal ‘22’
The type variable ‘a0’ is ambiguous

很容易看出为什么会这样——Haskell 允许数字字面值是具有 Num 实例的任何类型,在这里我们不能消除类型的歧义对于文字 0x16(或 22)应该是。

作为人类阅读我打算做的事情也很清楚——只有一个可用的 class C 实例满足 Num 约束,所以很明显我打算使用那个,所以 0x16 应该被视为 Word16

我知道有两种方法可以修复它:要么用它的类型注释文字:

main = return $ f (0x16 :: Word16)

或者定义一个基本上为您做注释的函数:

w16 x = x :: Word16
main = return $ f (w16 0x16)

我已经尝试了第三种方法,将 default (Word16) 放在文件的顶部,希望 Haskell 会选择它作为数字文字的默认类型,但我想我是误解了 default 关键字应该做什么,因为它不起作用。

我知道 typeclass 是开放的,所以只是因为你可以在上面引用的上下文中做出假设 Word16C 的唯一可能的数字实例不保存在其他模块中。但我的问题是:是否有某种机制可以让我 assume/enforce 得到 属性,这样就可以使用 f 并让 Haskell 解析其数字类型Word16 的参数在调用站点没有显式注释?

上下文是我正在实施 EDSL,当我知道我的参数将是 Word16 或其他一些非数字类型时,我宁愿不必包含手动类型提示。如果让 EDSL 感觉更自然,我愿意接受一些肮脏的 types/extensions 滥用!尽管如果解决方案确实涉及顽皮的编译指示,我肯定会感谢提示,提示我在使用它们时应该注意什么。

使用 GHC 7.10 "naughty pragmas" 的快速解决方案:

{-# LANGUAGE TypeFamilies, FlexibleInstances #-}

class    C a where f :: a -> ()
instance C T where f _ = ()
instance {-# INCOHERENT #-} (w ~ Word16) => C w where f _ = ()

以及 GHC 7.8:

{-# LANGUAGE TypeFamilies, FlexibleInstances, IncoherentInstances #-}

class    C a where f :: a -> ()
instance C T where f _ = ()
instance (w ~ Word16) => C w where f _ = ()

在这里,GHC 基本上选择了一个任意的最具体的实例,该实例在尝试统一实例头和约束后仍然存在。

你应该只在

时使用它
  • 您有一组固定的实例并且不导出 class。
  • 对于 class 方法的所有用例,有一个可能的最具体的实例(给定约束)。

许多人建议不要永远使用IncoherentInstances,但我认为如果我们遵守上述注意事项,它对于 DSL-s 会非常有趣。

对于其他想知道 default 的人(我知道我是!)

https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-750004.3

引用第 4.3.4 节:

In situations where an ambiguous type is discovered, an ambiguous type variable, v, is defaultable if:

  • v appears only in constraints of the form C v, where C is a class, and
  • at least one of these classes is a numeric class, (that is, Num or a subclass of Num), and
  • all of these classes are defined in the Prelude or a standard library.

这就解释了为什么您的 default 子句被完全忽略了; C不是标准库类型-class.

(至于为什么这是规则……这帮不了你。大概是为了避免破坏任意用户定义的代码。)