代码之家  ›  专栏  ›  技术社区  ›  n. m. could be an AI

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

  •  2
  • n. m. could be an AI  · 技术社区  · 6 年前

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

    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)
    

    一切都很好。除非我试着测试我的 diff 在GHCi中,我得到:

    *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 λ
    

    因此,我必须在类型对编译器来说应该是“明显”的地方编写类型签名。

    让我们试着帮点忙。

    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 a Diff (TimePoint a) (TimePoint a) (TimeInterval a) ,它们不能与现有的fundep共存。问题是,我如何解决这个问题?将数字包装成新类型不是一个可行的解决方案,我需要能够写 diff 5 3 diff time1 time2

    我知道我可以为 Diff Int Int Int Diff 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)
    
    2 回复  |  直到 6 年前
        1
  •  1
  •   leftaroundabout    6 年前

    问题是 Diff (TimePoint a) (TimePoint a) Diff a a Num a 约束,但请记住,您永远无法证明类型是 某个类的实例,因为该实例可能稍后仍会添加。

    Diff a a a 实例。而是定义 Diff Int Int Int Diff Double Double Double Diff Rational Rational Rational 分开。

        2
  •  1
  •   Isaac van Bakel gsiems    6 年前

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

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

    这需要 UndecidableInstances TypeFamilies 启用统一约束,除非 diff 最终是具体类型的,因此某种程度的推断(例如在GHCi中)是不可能的。