[我是“SYB Reloaded”论文的作者之一。]
TL;DR我们真的只是使用它,因为它对我们来说似乎更漂亮。基于类的Typeable
方法更实用。Spine
视图可以与类结合,Typeable
不依赖于Type
GADT。
该论文在其结论中指出了这一点:
我们的实现处理泛型编程的两个核心要素与原始 SYB 论文不同:我们使用具有显式类型参数的重载函数,而不是基于类型安全强制转换1或基于类的可扩展方案 [20] 的重载函数;我们使用显式脊椎视图而不是基于组合器的方法。这两个变化是相互独立的,并且是在明确的基础上做出的:我们认为 SYB 方法的结构在我们的环境中更加明显,并且与 PolyP 和 Generic Haskell 的关系变得更加清晰。我们发现,虽然脊椎视图仅限于可编写的通用函数类,但它适用于包括 GADT 在内的非常大的数据类型。
我们的方法不能轻易地用作库,因为使用显式类型参数对重载函数的编码需要 Type 数据类型和函数(如 toSpine)的可扩展性。但是,可以将 Spine 合并到 SYB 库中,同时仍然使用 SYB 论文的技术来编码重载函数。
因此,我们选择使用 GADT 进行类型表示主要是为了清楚起见。正如 Don 在他的回答中所说,这种表示有一些明显的优势,即它维护关于类型表示的类型的静态信息,并且它允许我们实现强制转换而无需任何进一步的魔法,特别是无需使用的unsafeCoerce
。类型索引函数也可以通过在类型上使用模式匹配直接实现,而无需回退到各种组合器,例如mkQ
or extQ
。
事实是我(我认为是合著者)根本就不是很喜欢这Typeable
门课。(事实上,我仍然没有,尽管它现在终于变得更加自律了,因为 GHC 添加了自动派生 for Typeable
,使其具有多态性,并最终消除了定义您自己的实例的可能性。)此外,Typeable
并没有现在那么成熟和广为人知,因此使用 GADT 编码来“解释”它似乎很有吸引力。而且,这个时候我们也在考虑给 Haskell 添加开放数据类型,从而缓解 GADT 封闭的限制。
所以,总结一下:如果你真的只需要一个封闭宇宙的动态类型信息,我总是选择 GADT,因为你可以使用模式匹配来定义类型索引函数,你不必依赖unsafeCoerce
也不需要高级编译器魔术。但是,如果宇宙是开放的,这很常见,当然对于通用编程设置,那么 GADT 方法可能是有指导意义的,但不实用,使用Typeable
是要走的路。
然而,正如我们在论文的结论中所说的那样,选择Type
overTypeable
并不是我们正在做出的另一个选择的先决条件,即使用Spine
视图,我认为这是更重要的,也是论文的核心.
论文本身(在第 8 节中)展示了一个受“Scrap your Boilerplate with Class”论文启发的变体,它使用了一个Spine
带有类约束的视图。但是我们也可以做一个更直接的开发,我将在下面展示。为此,我们将使用Typeable
from 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)
正如我上面所说,如果我们现在想要定义一个通用的 sum 函数来总结所有Int
出现的事件,我们不能再进行模式匹配,而是必须回退到mkQ
,但是mkQ
纯粹根据Typeable
并且完全独立于 来定义Spine
:
mkQ :: (Typeable a, Typeable b) => r -> (b -> r) -> a -> r
(r `mkQ` br) a = maybe r br (cast a)
然后(再次与原始 SYB 完全相同):
sum :: Query Int
sum = everything (+) sumQ
sumQ :: Query Int
sumQ = mkQ 0 id
对于本文后面的一些内容(例如,添加构造函数信息),需要做更多的工作,但都可以完成。所以使用Spine
真的完全不依赖于使用Type
。