我可以自动为转换函数生成类型类实例而不会过于宽松吗?



>最近,我问了一个关于我创建的生成运行时无限循环的实例的问题,我得到了一个很棒的答案!现在我明白了发生了什么,我有一个新的问题:我能解决我实现最初目标的尝试吗?

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

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)

不幸的是,这个实例太宽松了,当提供两种我认为应该无效的类型时,它会导致生成的代码递归 - 我尚未定义规范转换的类型。


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

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(和类似)实例。

Haskell类型系统非常令人印象深刻,但我担心在这种情况下,它的实例解析规则不够强大,无法同时完成所有这些目标。

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杂注表示我们希望覆盖默认实例。或者,我们可以在默认实例上放置一个overlappable杂注,但这更危险,因为这会让所有其他实例(可能在外部模块中定义)能够静默覆盖。

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

您可以使用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

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

相关内容

  • 没有找到相关文章

最新更新