TypeRep与“Type”GADT的关系

时间:2013-03-01 14:46:49

标签: haskell scrap-your-boilerplate

Scrap your boilerplate reloaded中,作者描述了Scrap Your Boilerplate的新表现形式,它应该与原作相同。

然而,一个不同之处在于他们假设一组有限的,基本的“基础”类型,用GADT编码

data Type :: * -> * where
  Int :: Type Int
  List :: Type a -> Type [a]
  ...

在原始SYB中,使用类型安全转换,使用Typeable类实现。

我的问题是:

  • 这两种方法之间有什么关系?
  • 为什么选择GADT代表作为“SYB Reloaded”演示文稿?

2 个答案:

答案 0 :(得分:4)

嗯,显然Typeable使用是开放的 - 事后可以添加新的变体,而无需修改原始定义。

但重要的变化是,TypeRep是无类型的。也就是说,运行时类型TypeRep与其编码的静态类型之间没有任何关联。使用GADT方法,我们可以对GADT a给出的Type类型及其Type a之间的映射进行编码。

因此,我们为类型rep静态链接到其原始类型提供证据,并且可以使用Type a编写静态类型的动态应用程序(例如)作为我们具有运行时a的证据。

在较旧的TypeRep案例中,我们没有这样的证据,它归结为运行时字符串的相等性,以及通过fromDynamic的强制性和希望。

比较签名:

toDyn :: Typeable a => a -> TypeRep -> Dynamic

与GADT风格:

toDyn :: Type a => a -> Type a -> Dynamic

我无法伪造我的类型证据,我可以在以后重构事物时使用它,例如当我拥有a时,查找Type a的类型类实例。

答案 1 :(得分:4)

[我是" SYB Reloaded&#34的作者之一;纸。]

TL; DR 我们真的只是用它,因为它对我们来说似乎更漂亮。基于类的Typeable方法更实用。 Spine视图可以与Typeable类合并,而不依赖于Type GADT。

该论文在其结论中陈述了这一点:

  

我们的实现处理泛型编程的两个核心要素,与原始的SYB文件不同:我们使用重载函数   显式类型参数,而不是基于类型安全的重载函数   cast 1或基于类的可扩展方案[20];我们使用显式脊柱   查看而不是基于组合器的方法。两个变化都是独立的   彼此之间,并且已经清晰地记在心里:我们认为SYB方法的结构在我们的环境中更加明显,并且这种关系   PolyP和Generic Haskell变得更加清晰。我们已经透露了   脊柱视图在可编写的泛型函数类中是有限的,它是   适用于非常大类的数据类型,包括GADT。

     

我们的方法不能轻易用作库,因为编码了   使用显式类型参数重载函数需要可扩展性   Type数据类型和toSpine等函数。但是,可以将Spine合并到SYB库中,同时仍然使用SYB的技术   用于编码重载函数的论文。

因此,使用GADT进行类型表示的选择主要是为了清晰起见。正如唐在他的回答中所说,这种表示有一些明显的优点,即它维护关于类型表示的类型的静态信息,并且它允许我们在没有任何进一步魔法的情况下实现强制转换,特别是没有使用unsafeCoerce。类型索引函数也可以通过在类型上使用模式匹配直接实现,而不会回退到mkQextQ等各种组合器。

事实是我(我认为共同作者)根本不是非常喜欢Typeable类。 (事实上​​,我还没有,尽管现在GHC为Typeable添加了自动派生,但最终变得更加自律,使它变得有点多态,最终会消除定义的可能性你自己的实例。)另外,Typeable并不像现在那样已经建立和广为人知,所以它似乎很有吸引力,并且#34;解释"它通过使用GADT编码。此外,这是我们考虑将open datatypes添加到Haskell的时候,从而减轻了GADT关闭的限制。

因此,总结一下:如果您实际上只需要一个封闭的Universe的动态类型信息,我总是选择GADT,因为您可以使用模式匹配来定义类型索引函数,而您没有依赖unsafeCoerce或高级编译器魔术。然而,如果宇宙是开放的,这对于通用编程设置来说是非常普遍的,那么GADT方法可能是有益的,但是不实用,并且使用Typeable是可行的方法。 / p>

然而,正如我们在论文的结论中所述,Type超过Typeable的选择并不是我们正在制定的另一个选择的先决条件,即使用Spine视图,我认为这个视图更重要,也是论文的核心。

该论文本身(在第8节中)显示了受"Scrap your Boilerplate with Class"论文启发的变体,该论文使用带有类约束的Spine视图。但我们也可以进行更直接的开发,我将在下面展示。为此,我们将使用Typeable中的Data.Typeable,但定义我们自己的Data类,为简单起见,它只包含toSpine方法:

class Typeable a => Data a where
  toSpine :: a -> Spine a

Spine数据类型现在使用Data约束:

data Spine :: * -> * where
  Constr  :: a -> Spine a
  (:<>:)  :: (Data a) => Spine (a -> b) -> a -> Spine b

函数fromSpine与其他表示一样简单:

fromSpine :: Spine a -> a
fromSpine (Constr x) = x
fromSpine (c :<>: x) = fromSpine c x

对于Data等平面类型,Int的实例是微不足道的:

instance Data Int where
  toSpine = Constr

对于像二叉树这样的结构化类型,它们仍然完全是直截了当的:

data Tree a = Empty | Node (Tree a) a (Tree a)

instance Data a => Data (Tree a) where
  toSpine Empty        = Constr Empty
  toSpine (Node l x r) = Constr Node :<>: l :<>: x :<>: r

然后,本文继续介绍各种通用函数,例如mapQ。这些定义很难改变。我们只获得Data a =>的类约束,其中论文的函数参数为Type a ->

mapQ :: Query r -> Query [r]
mapQ q = mapQ' q . toSpine

mapQ' :: Query r -> (forall a. Spine a -> [r])
mapQ' q (Constr c) = []
mapQ' q (f :<>: x) = mapQ' q f ++ [q x]

诸如everything之类的高级函数也会丢失它们的显式类型参数(然后实际上看起来与原始SYB完全相同):

everything :: (r -> r -> r) -> Query r -> Query r
everything op q x = foldl op (q x) (mapQ (everything op q) x)

正如我上面所说,如果我们现在想要定义一个总和函数来总结所有Int次出现,我们就不能再模式匹配,但必须回到mkQ,但是{{1 }}纯粹根据mkQ定义,完全独立于Typeable

Spine

然后(再次与原始SYB完全一样):

mkQ :: (Typeable a, Typeable b) => r -> (b -> r) -> a -> r
(r `mkQ` br) a = maybe r br (cast a)

对于本文后面的一些内容(例如,添加构造函数信息),需要做更多的工作,但这一切都可以完成。因此,使用sum :: Query Int sum = everything (+) sumQ sumQ :: Query Int sumQ = mkQ 0 id 实际上并不依赖于使用Spine