为异构操作创建类型 class 时出现问题

Problem with creating a type class for a heterogenous operation

接下来是我正在尝试做的一个非常简化的版本。

假设我想创建一个可以接受不同类型操作数的通用差分运算。

class Diff a b c where
    diff :: a -> b -> c

当然我们可以将此操作应用于数字。

instance Num a ⇒ Diff a a a where
    diff = (-)

但不仅仅是数字。如果我们说两个时间点,那么它们之间的差异就是一个时间间隔。

newtype TimePoint = TP Integer deriving Show -- seconds since epoch
newtype TimeInterval = TI Integer deriving Show -- in seconds

instance Diff TimePoint TimePoint TimeInterval where
    diff (Tp x) (Tp y) = TI (x-y)

一切都很好。除了当我尝试在 GHCi 中测试我的 diff 时,我得到这个:

*Example λ diff 5 3

<interactive>:1:1: error:
    • Could not deduce (Diff a0 b0 c)
      from the context: (Diff a b c, Num a, Num b)
        bound by the inferred type for ‘it’:
                   forall a b c. (Diff a b c, Num a, Num b) => c
        at <interactive>:1:1-8
      The type variables ‘a0’, ‘b0’ are ambiguous
    • In the ambiguity check for the inferred type for ‘it’
      To defer the ambiguity check to use sites, enable AllowAmbiguousTypes
      When checking the inferred type
        it :: forall a b c. (Diff a b c, Num a, Num b) => c
*Example λ

所以我必须在类型应该为编译器 "obvious" 的地方写类型签名。

让我们试着帮助它一下。

class Diff a b c | a b -> c where
  diff ∷ a -> b -> c

现在应该可以判断结果的类型了吧!不幸的是,这无法编译:

[1 of 1] Compiling Example          ( Example.hs, interpreted )

Example.hs:8:10: error:
    Functional dependencies conflict between instance declarations:
      instance Num a => Diff a a a -- Defined at Example.hs:8:10
      instance Num a => Diff (TimePoint a) (TimePoint a) (TimeInterval a)
        -- Defined at Example.hs:14:10
  |
8 | instance Num a => Diff a a a where
  |          ^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.
Prelude GOA λ 

顺便说一句,我也尝试用关联类型族代替 fundeps,结果与预期相似。

现在我完全明白为什么会这样了。有两个实例 Diff a a aDiff (TimePoint a) (TimePoint a) (TimeInterval a),它们不能与 fundep 共存。问题是,我该如何解决这个问题?在新类型中包装数字不是一个可行的解决方案,我需要能够编写 diff 5 3diff time1 time2,并且这些表达式的类型应该从操作数中推导出来。

我知道我可以为 Diff Int Int IntDiff Double Double Double 以及 Diff Rational Rational Rational 定义单独的实例,但这不是一个理想的解决方案,因为 Num 的新实例可能是定义并且代码必须处理它们,而不必为每个 Diff 定义一个额外的实例。

下面是一个最小的完整示例:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances #-}

module Example where

class Diff a b c | a b -> c where
  diff :: a -> b -> c

instance Num a => Diff a a a where
  diff = (-)

data TimePoint a = TP a deriving Show
data TimeInterval a = TI a deriving Show

instance Num a => Diff (TimePoint a) (TimePoint a) (TimeInterval a) where
  diff (TP x) (TP y) = TI (x - y)

问题是 Diff (TimePoint a) (TimePoint a) 恰好是 Diff a a 的特例。您可能会认为“这不是因为 Num a 约束”,但请记住,您永远无法证明一个类型 不是 某些 class 的实例,因为稍后可能仍会添加该实例。

解决方案是不定义 Diff a a a 实例。而是分别定义 Diff Int Int IntDiff Double Double Double 以及 Diff Rational Rational Rational

您可以尝试一个常见的技巧来避免@leftaroundabout 在他们的回答中描述的头部匹配问题

instance {-# OVERLAPPABLE #-} (a ~ b, a ~ c, Num a) => Diff a b c where
    diff = (-)

这需要 UndecidableInstancesTypeFamilies 来启用统一约束,除非 diff 的结果最终被具体类型化,否则将无法工作,因此某种程度的推理,例如在 GHCi 中,这是不可能的。