我是否可以自动为转换函数生成类型类实例而不过度宽容?

时间:2016-05-24 17:14:03

标签: haskell typeclass undecidable-instances

最近,I asked a question about an instance I had created generating a runtime infinite loop,我得到了一个很棒的答案!现在我明白了发生了什么,我有一个新问题:我可以修复我完成原定目标的尝试吗?

让我重申并澄清我的问题是什么:我想创建一个类型类,用于在我的代码中转换一些等效的数据类型。我创建的类型类非常简单且非常通用:它包含一个在任意数据类型之间转换的转换函数:

class Convert a b where
  convert :: a -> b

但是,此类型类具有特定目的:在具有规范表示的特定值类别之间进行转换。因此,有一个特定的数据类型是“规范的”,我想使用此数据类型的属性来减轻我的类型类的实现者的负担。

data Canonical = ...

class ConvertRep a b where
  convertRep :: a -> b

具体来说,请考虑两种不同的表示形式,RepARepB。我可以合理地定义一些实例来将这些表示转换为Canonical

instance ConvertRep RepA Canonical where ...
instance ConvertRep Canonical RepA where ...

instance ConvertRep RepB Canonical where ...
instance ConvertRep Canonical RepB where ...

现在,这很有用,因为我现在可以将convertRep用于两种表示形式,但它主要用作重载convertRep名称的方法。我想做更强大的事情:毕竟,我现在已经有效地定义了以下类型的四个函数:

RepA      -> Canonical
Canonical -> RepA
RepB      -> Canonical
Canonical -> RepB

根据这些定义,我似乎也应该能够生成以下类型的两个函数,这似乎是合理的:

RepA -> RepB
RepB -> RepA

基本上,由于两种数据类型都可以转换为规范表示/从规范表示转换,我希望自动直接相互生成转换函数。正如我在上述问题中提到的那样,我的尝试看起来像这样:

instance (ConvertRep a Canonical, ConvertRep Canonical b) => ConvertRep a b where
  convertRep = convertRep . (convertRep :: a -> Canonical)

不幸的是,这个实例太宽容了,causes the generated code to recurse when provided two types I think should be invalid - 类型我还没有定义规范转换。

为了尝试解决这个问题,我考虑了另一种更简单的方法。我想我可以使用两个类型而不是一个来防止这种递归问题:

class ToCanonical a where
  toCanonical :: a -> Canonical

class FromCanonical a where
  fromCanonical :: Canonical -> a

现在可以定义一个新函数来执行我最初感兴趣的convertRep转换:

convertRep :: (ToCanonical a, FromCanonical b) => a -> b
convertRep = fromCanonical . toCanonical

然而,这是以灵活性为代价的:不再可能在两个非规范表示之间创建直接转换实例。

例如,也许我知道RepARepB将经常互换使用,因此它们之间会相互转换很多。因此,转换为/ Canonical的额外步骤是浪费时间。我想可选定义直接转换实例:

instance ConvertRep RepA RepB where
  convertRep = ...

提供两种常见类型之间的“快速路径”转换。

总结一下:有没有办法使用Haskell的类型系统来实现所有这些目标?

  1. “生成”表示之间的转换,给出在表示和规范形式之间转换的函数。
  2. 可选择在通常转换的实例之间提供“快速路径”。
  3. 拒绝尚未明确定义的实例;也就是说,不允许创建无限复发并产生底部的ConvertRep Canonical Canonical(和类似的)实例。
  4. Haskell类型系统令人印象深刻,但我担心在这种情况下,它的实例解析规则不够强大,无法立即实现所有这些目标。

2 个答案:

答案 0 :(得分:4)

OverlappingInstances可用于后退行为:

data A = A deriving (Show)
data B = B deriving (Show)
data C = C deriving (Show) -- canonical

class Canonical a where
  toC   :: a -> C
  fromC :: C -> a

class Conv a b where
  to   :: a -> b
  from :: b -> a

instance (Canonical a, Canonical b) => Conv a b where
  to   = fromC . toC
  from = fromC . toC

instance {-# overlapping #-} Conv A B where
  to   _ = B
  from _ = A

instance Canonical A where
  toC _ = C
  fromC _ = A

instance Canonical B where
  toC _ = C
  fromC _ = B
如果存在直接实例,

Conv会直接转换,或者通过C返回转换。 overlapping pragma信号表示我们希望覆盖默认实例。或者,我们可以在默认实例上放置overlappable pragma,但这更危险,因为这样可以让所有其他实例(可能在外部模块中定义)能够以静默方式覆盖。

但是,我不认为这种转换方案特别有用或者是良好的做法。重叠实例会导致在不同模块中解析不同实例的风险,并且它们最终可能通过导入和存在实例进入同一模块,从而可能造成麻烦。

答案 1 :(得分:2)

您可以使用DefaultSignatures指定默认实现。虽然这仍然迫使您手动列出所有有效的转换,但它相对紧凑,因为您可以依赖默认实现。即

之类的东西
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}

data Canonical = C

class ConvertRep a b where
  convertRep :: a -> b
  default convertRep :: (ConvertRep a Canonical, ConvertRep Canonical b) => a -> b
  convertRep = convertRep . (convertRep :: a -> Canonical)

data A = A
data B = B

instance ConvertRep A Canonical where
  convertRep A = C

instance ConvertRep Canonical B where
  convertRep C = B

现在,您可以使用

定义AB之间的转化
instance ConvertRep A B

但仍然可以基于每种类型覆盖默认实现。