这三个/为什么有什么区别? GADT(和常规数据类型)只是数据系列的简写吗?具体来说,两者之间有什么区别
data GADT a where
MkGADT :: Int -> GADT Int
data family FGADT a
data instance FGADT a where -- note not FGADT Int
MkFGADT :: Int -> FGADT Int
data family DF a
data instance DF Int where -- using GADT syntax, but not a GADT
MkDF :: Int -> DF Int
(这些示例是否被过度简化,所以我看不到差异的微妙之处?)
数据族是可扩展的,但GADT不是。 OTOH数据系列实例不得重叠。因此,我无法为FGADT
声明另一个实例/任何其他构造函数;就像我无法为GADT
声明任何其他构造函数一样。我可以为DF
声明其他实例。
在这些构造函数上进行模式匹配后,等式的rhs会“知道”有效载荷为Int
。
对于类实例(我很惊讶地发现),我可以编写重叠的实例来消耗GADT:
instance C (GADT a) ...
instance {-# OVERLAPPING #-} C (GADT Int) ...
,对于(FGADT a), (FGADT Int)
同样。但是不是(DF a)
的地方:它必须是(DF Int)
的地方-这才有意义;没有data instance DF a
,如果有的话就会重叠。
添加:阐明@kabuhr的答案(谢谢)
与我认为您在部分问题中声称的相反,对于纯数据系列,在构造函数上进行匹配不会执行任何推断
这些类型很棘手,所以我希望我需要显式签名才能使用它们。在那种情况下,简单的数据族最简单
inferDF (MkDF x) = x -- works without a signature
推断的类型inferDF :: DF Int -> Int
是有意义的。给它签名inferDF :: DF a -> a
是没有道理的:data instance DF a ...
没有声明。与foodouble :: Foo Char a -> a
类似,也没有data instance Foo Char a ...
。
GADT很尴尬,我已经知道了。因此,没有明确的签名,这两种方法都不起作用
inferGADT (MkGADT x) = x
inferFGADT (MkFGADT x) = x
如您所说,神秘的“无法触及”的信息。我在“匹配这些构造函数”注释中的意思是:编译器“知道”等式的rhs,有效载荷为Int
(对于所有三个构造函数),因此最好获得与那个。
那我在想data GADT a where ...
就像data instance GADT a where ...
。我可以给签名inferGADT :: GADT a -> a
或inferGADT :: GADT Int -> Int
(对于inferFGADT
也一样)。那是有道理的:有一个data instance GADT a ...
或我可以给一个更特定的类型的签名。
因此,在某些方面,数据族是GADT的概括。我也如你所说
因此,在某些方面,GADT是数据族的概括。
嗯。 (问题背后的原因是,GHC Haskell已经到了功能膨胀的阶段:有太多相似但又不同的扩展。我试图将其简化为更少的基础抽象。然后@HTNW的解释方法就进一步扩展而言,这将对学习者有所帮助;应删除IMO中数据类型的存在性:改为使用GADT;应根据数据类型和它们之间的映射函数来解释PatternSynonym,而不是相反。哦,还有一些DataKinds的东西,我在初读时就跳过了。)
答案 0 :(得分:6)
首先,您应该将数据族视为独立的ADT的集合,这些ADT恰好按类型进行了索引,而GADT是具有可推断类型参数的单个数据类型,其中该参数受到约束(通常是,像a ~ Int
这样的等式约束可以通过模式匹配引入范围。
这意味着最大的区别是,与我认为您在部分问题中声称的相反,对于纯数据系列,在构造函数上进行匹配不会不对类型参数。特别是,此类型检查:
inferGADT :: GADT a -> a
inferGADT (MkGADT n) = n
但这不是 :
inferDF :: DF a -> a
inferDF (MkDF n) = n
并且没有类型签名,第一个将无法键入检查(带有神秘的“无法触摸”消息),而第二个将被推断为DF Int -> Int
。
对于类似FGADT
类型的东西,将数据族与GADT相结合的情况,情况变得更加混乱,我承认我还没有真正考虑过它的详细工作方式。但是,作为一个有趣的示例,请考虑:
data family Foo a b
data instance Foo Int a where
Bar :: Double -> Foo Int Double
Baz :: String -> Foo Int String
data instance Foo Char Double where
Quux :: Double -> Foo Char Double
data instance Foo Char String where
Zlorf :: String -> Foo Char String
在这种情况下,Foo Int a
是带有不可推论的a
参数的GADT:
fooint :: Foo Int a -> a
fooint (Bar x) = x + 1.0
fooint (Baz x) = x ++ "ish"
但是Foo Char a
只是单独的ADT的集合,因此不会进行类型检查:
foodouble :: Foo Char a -> a
foodouble (Quux x) = x
出于同样的原因,inferDF
不会在上面进行类型检查。
现在,回到普通的DF
和GADT
类型,只需使用DFs
就可以在很大程度上模拟GADTs
。例如,如果您有DF:
data family MyDF a
data instance MyDF Int where
IntLit :: Int -> MyDF Int
IntAdd :: MyDF Int -> MyDF Int -> MyDF Int
data instance MyDF Bool where
Positive :: MyDF Int -> MyDF Bool
您只需编写各个构造函数块即可将其编写为GADT:
data MyGADT a where
-- MyGADT Int
IntLit' :: Int -> MyGADT Int
IntAdd' :: MyGADT Int -> MyGADT Int -> MyGADT Int
-- MyGADT Bool
Positive' :: MyGADT Int -> MyGADT Bool
因此,在某些方面,GADT是数据族的概括。但是,数据族的主要用例是为类定义关联的数据类型:
class MyClass a where
data family MyRep a
instance MyClass Int where
data instance MyRep Int = ...
instance MyClass String where
data instance MyRep String = ...
需要数据系列的“开放”性质(而GADT基于模式的推理方法无济于事)。
答案 1 :(得分:3)
我认为,如果我们为数据构造函数使用PatternSynonyms
样式的类型签名,则区别会很明显。让我们从Haskell 98开始
data D a = D a a
您会得到一种图案类型:
pattern D :: forall a. a -> a -> D a
可以从两个方向读取它。 D
在“转发”或表达式上下文中说:“ forall a
,您可以给我2个a
,而我给您一个D a
”。作为一种模式,“向后”表示“ forall a
,您可以给我一个D a
,我给您2个a
”。
现在,您在GADT定义中编写的内容不是模式类型。这些是什么?说谎说谎撒谎。仅在替代方法是使用ExistentialQuantification
手动将其写出时,才给予他们注意。让我们用这个
data GD a where
GD :: Int -> GD Int
你得到
-- vv ignore
pattern GD :: forall a. () => (a ~ Int) => Int -> GD a
这说:forall a
,您可以给我一个GD a
,我可以给您一个证明,证明a ~ Int
加上Int
。
重要观察:GADT构造函数的返回/匹配类型始终是“数据类型头”。我定义了data GD a where ...
;我得到了GD :: forall a. ... GD a
。对于Haskell 98构造函数和data family
构造函数来说,也是如此,尽管它有些微妙。
如果我有一个GD a
,但我不知道a
是什么,即使我写了GD
,我仍然可以传入GD :: Int -> GD Int
,这似乎说我只能与GD Int
相匹配。这就是为什么我说GADT构造函数在说谎。模式类型永远不会说谎。它清楚地表明,forall a
,我可以将GD a
与GD
构造函数匹配,并得到a ~ Int
和值Int
的证据。
好,data family
秒。暂时不要将它们与GADT混合使用。
data Nat = Z | S Nat
data Vect (n :: Nat) (a :: Type) :: Type where
VNil :: Vect Z a
VCons :: a -> Vect n a -> Vect (S n) a -- try finding the pattern types for these btw
data family Rect (ns :: [Nat]) (a :: Type) :: Type
newtype instance Rect '[] a = RectNil a
newtype instance Rect (n : ns) a = RectCons (Vect n (Rect ns a))
现在实际上有两个数据类型头。正如@ K.A.Buhr所说,不同的data instance
的行为就像只是 happen 共享名称的不同数据类型。模式类型是
pattern RectNil :: forall a. a -> Rect '[] a
pattern RectCons :: forall n ns a. Vect n (Rect ns a) -> Rect (n : ns) a
如果我有一个Rect ns a
,但我不知道ns
是什么,我无法匹配它。 RectNil
只需要Rect '[] a
s,RectCons
只需要Rect (n : ns) a
s。您可能会问:“我为什么要减少功率?” @KABuhr给出了一个:GADT是封闭的(并且有充分的理由;请继续关注),家庭是开放的。在Rect
的情况下,这并不成立,因为这些实例已经填满了整个[Nat] * Type
的空间。原因实际上是newtype
。
这是GADT RectG
:
data RectG :: [Nat] -> Type -> Type where
RectGN :: a -> RectG '[] a
RectGC :: Vect n (RectG ns a) -> RectG (n : ns) a
我明白了
-- it's fine if you don't get these
pattern RectGN :: forall ns a. () => (ns ~ '[]) => a -> RectG ns a
pattern RectGC :: forall ns' a. forall n ns. (ns' ~ (n : ns)) =>
Vect n (RectG ns a) -> RectG ns' a
-- just note that they both have the same matched type
-- which means there needs to be a runtime distinguishment
如果我有一个RectG ns a
并且不知道ns
是什么,我仍然可以在上面进行匹配。编译器必须使用数据构造函数保留此信息。因此,如果我有一个RectG [1000, 1000] Int
,则将产生一百万个RectGN
构造函数的开销,这些构造函数全部“保留”相同的“信息”。不过,Rect [1000, 1000] Int
很好,因为我无法匹配并判断Rect
是RectNil
还是RectCons
。这使构造函数成为newtype
,因为它不包含任何信息。我会改用其他的GADT,就像
data SingListNat :: [Nat] -> Type where
SLNN :: SingListNat '[]
SLNCZ :: SingListNat ns -> SingListNat (Z : ns)
SLNCS :: SingListNat (n : ns) -> SingListNat (S n : ns)
,它在Rect
空间而不是O(sum ns)
空间中存储O(product ns)
的尺寸(我认为这是正确的)。这也是GADT
封闭而家庭开放的原因。 GADT与普通的data
类型相似,但它具有相等的证据和存在性。向GADT添加构造函数比向Haskell 98类型添加构造函数更有意义,因为任何不知道其中一个构造函数的代码都属于非常不好的时光。不过,这对于家庭来说很好,因为正如您所注意到的,一旦定义了家庭的分支,就不能在该分支中添加更多的构造函数。一旦知道了所处的分支,便知道了构造函数,没有人能打破它。如果您不知道要使用哪个分支,则不得使用任何构造函数。
您的示例并未真正将GADT和数据系列混在一起。模式类型很漂亮,因为它们可以规范化data
定义中的表面差异,所以让我们看一下。
data family FGADT a
data instance FGADT a where
MkFGADT :: Int -> FGADT Int
给你
pattern MkFGADT :: forall a. () => (a ~ Int) => Int -> FGADT a
-- no different from a GADT; data family does nothing
但是
data family DF a
data instance DF Int where
MkDF :: Int -> DF Int
给予
pattern MkDF :: Int -> DF Int
-- GADT syntax did nothing
这是适当的混合方式
data family Map k :: Type -> Type
data instance Map Word8 :: Type -> Type where
MW8BitSet :: BitSet8 -> Map Word8 Bool
MW8General :: GeneralMap Word8 a -> Map Word8 a
哪个提供模式
pattern MW8BitSet :: forall a. () => (a ~ Bool) => BitSet8 -> Map Word8 a
pattern MW8General :: forall a. GeneralMap Word8 a -> Map Word8 a
如果我有一个Map k v
,但我不知道k
是什么,那么我就无法将其与MW8General
或MW8BitSet
进行匹配,因为那些人只想{ {1}}个。这就是Map Word8
的影响。如果我有一个data family
,但我不知道Map Word8 v
是什么,则构造函数上的匹配可以向我揭示它是v
还是其他的东西。