定义部分应用的类型类

Defining partially applied typeclasses

探索typeclasses are essentially C++ abstract classes without nested inheritance的思路,我写了typeclass

class Interface i c where
    i :: c -> i

instance Interface i i where i = id

infixl 1 #
(#) :: Interface i c => c -> (i -> r) -> r
c # f = f $ i c

界面类似

data IDrawable' = IDrawable { draw :: IO () }

我想要类似的东西

type IDrawable c = Interface IDrawable' c

所以我可以做

data Object = Object { objectDraw :: IO () }
data Person = Person { personDraw :: IO () }

instance IDrawable Object where i = IDrawable . objectDraw
instance IDrawable Person where i = IDrawable . personDraw

虽然 type IDrawable c 使用 ConstraintKinds 编译,但我不允许使用错误

执行 instance IDrawable Object where i = IDrawable . objectDraw
'i' is not a (visible) method of class 'IDrawable`

有没有办法声明 IDrawable c = Interface IDrawable' c 以便它可以被实例化?

这纯粹是出于学术兴趣,我不建议任何人在实际应用中使用这种模式,我只是想知道在不应用 TemplateHaskell 或 [=20 的情况下这种事情是否可行=].

您可以声明一个没有实例的 "partial" class:

class Interface IDrawable' c => IDrawable c

instance Interface IDrawable' Object where i = IDrawable . objectDraw
instance Interface IDrawable' Person where i = IDrawable . personDraw

或者,可以使用约束同义词:

type IDrawable c = Interface IDrawable' c

classy 解决方案可能更可取,因为 IDrawable class 具有适当的 * -> Constraint 类型,而类型同义词除非完全应用,否则无法使用。这可能是相关的,因为数据定义(以及类型族和几乎所有类型级别的 hackery)只能使用适当的类型构造函数。

不,这是不可能的(从 7.8.3 开始,我认为也是 7.10);是 GHC bug #7543。这不是一个被大量贩卖的错误;显然至少有一些人希望能够写出这种东西(例如,你,Edward Kmett),但大多数人都没有注意到这一点。跟踪器上记录的更改此行为没有任何进展。

至于为什么你不能,让我解释一下 Simon Peyton-Jones 在错误跟踪器上的解释。问题在于类型检查实例有两个部分:首先,GHC 必须查找方法名称(此处为 i)的来源;其次,GHC 必须扩展类型同义词。因为这两个步骤在这个问题中是由 GHC 的两个不同组件执行的,所以不能支持约束同义词实例; GHC 无法判断它需要查找什么 class 才能找到 i.


这是一个错误的另一个原因——根据 上的评论,我发现这个的原因——是当前的行为不像 "it doesn't work" 那样简单。相反,它 尝试 工作,但你不能声明任何方法……但你 可以 声明一个无方法的实例!在 GHCi 中:

GHCi, version 7.8.3: http://www.haskell.org/ghc/  :? for help
...
Prelude> :set -XMultiParamTypeClasses -XFlexibleInstances -XConstraintKinds
Prelude> class Interface i c where i :: c -> i
Prelude> instance Interface i i where i = id
Prelude> let (#) :: Interface i c => c -> (i -> r) -> r ; c # f = f $ i c ; infixl 1 #
Prelude> data IDrawable' = IDrawable { draw :: IO () }
Prelude> type IDrawable = Interface IDrawable'
Prelude> instance IDrawable () where i _ = IDrawable $ return ()

<interactive>:8:29:
    ‘i’ is not a (visible) method of class ‘IDrawable’
Prelude> ()#draw

<interactive>:9:3:
    No instance for (Interface IDrawable' ()) arising from a use of ‘#’
    In the expression: () # draw
    In an equation for ‘it’: it = () # draw
Prelude> instance IDrawable () where {}

<interactive>:10:10: Warning:
    No explicit implementation for
      ‘i’
    In the instance declaration for ‘Interface IDrawable' ()’
Prelude> ()#draw
*** Exception: <interactive>:10:10-21: No instance nor default method for class operation Ghci1.i

换句话说:

instance IDrawable () where i _ = IDrawable $ return ()

失败,但是

instance IDrawable () where {}

成功!很明显,检查需要放松或收紧,具体取决于所需的行为:-)


P.S.: 还有一件事:您应该始终尽可能减少 eta-reduce 类型同义词。这就是我将 IDrawable 更改为

的原因
type IDrawable = Interface IDrawable'

并在上面的 GHCi 代码的两边删除了 c 参数。这样做的好处是,由于不能部分应用类型同义词,因此您不能将 IDrawable 的版本作为参数传递给任何东西;然而,完全缩减 eta 的版本可以传递到任何地方,只要有类似的东西 * -> Constraint.

(在 中提到了这一点,我在那里的评论中提到了这一点;不过,由于我最终也写了一个答案,所以我想我也应该在这里添加它。)