差异:GADT,数据系列,属于GADT的数据系列

时间:2018-09-17 12:19:08

标签: haskell gadt

这三个/为什么有什么区别? 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 -> ainferGADT :: GADT Int -> Int(对于inferFGADT也一样)。那是有道理的:有一个data instance GADT a ...或我可以给一个更特定的类型的签名。

因此,在某些方面,数据族是GADT的概括。我也如你所说

  

因此,在某些方面,GADT是数据族的概括。

嗯。 (问题背后的原因是,GHC Haskell已经到了功能膨胀的阶段:有太多相似但又不同的扩展。我试图将其简化为更少的基础抽象。然后@HTNW的解释方法就进一步扩展而言,这将对学习者有所帮助;应删除IMO中数据类型的存在性:改为使用GADT;应根据数据类型和它们之间的映射函数来解释PatternSynonym,而不是相反。哦,还有一些DataKinds的东西,我在初读时就跳过了。)

2 个答案:

答案 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不会在上面进行类型检查。

现在,回到普通的DFGADT类型,只需使用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 aGD构造函数匹配,并得到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很好,因为我无法匹配并判断RectRectNil还是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是什么,那么我就无法将其与MW8GeneralMW8BitSet进行匹配,因为那些人​​只想{ {1}}个。这就是Map Word8的影响。如果我有一个data family,但我不知道Map Word8 v是什么,则构造函数上的匹配可以向我揭示它是v还是其他的东西。