在Scrap your boilerplate reloaded中,作者描述了Scrap Your Boilerplate的新表现形式,它应该与原作相同。
然而,一个不同之处在于他们假设一组有限的,基本的“基础”类型,用GADT编码
data Type :: * -> * where
Int :: Type Int
List :: Type a -> Type [a]
...
在原始SYB中,使用类型安全转换,使用Typeable
类实现。
我的问题是:
答案 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
。类型索引函数也可以通过在类型上使用模式匹配直接实现,而不会回退到mkQ
或extQ
等各种组合器。
事实是我(我认为共同作者)根本不是非常喜欢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
。