如何使用广义代数数据类型?
haskell wikibook中给出的例子太短,无法让我了解GADT的真正可能性。
答案 0 :(得分:51)
GADT是来自依赖类型语言的归纳家族的弱近似 - 所以让我们从那里开始。
归纳族是依赖类型语言的核心数据类型引入方法。例如,在Agda中,您可以定义像这样的自然数
data Nat : Set where
zero : Nat
succ : Nat -> Nat
这不是很奇特,它与Haskell定义基本上是一样的
data Nat = Zero | Succ Nat
实际上在GADT语法中,Haskell形式更加相似
{-# LANGUAGE GADTs #-}
data Nat where
Zero :: Nat
Succ :: Nat -> Nat
所以,乍一看你可能会认为GADT只是简洁的额外语法。这只是冰山一角。
Agda有能力代表Haskell程序员不熟悉和陌生的各种类型。一个简单的是有限集的类型。此类型的编写方式与Fin 3
类似,代表数字{0, 1, 2}
的集。同样,Fin 5
代表数字{0,1,2,3,4}
。
在这一点上,这应该是非常奇怪的。首先,我们指的是具有常规数字的类型"类型"参数。其次,不清楚Fin n
代表集{0,1...n}
意味着什么。在真正的Agda中,我们做了更强大的事情,但只要说我们可以定义contains
函数
contains : Nat -> Fin n -> Bool
contains i f = ?
现在这又是奇怪的,因为"自然" contains
的定义类似于i < n
,但n
是仅存在于Fin n
类型中的值,我们不应该跨越这个鸿沟很容易虽然事实证明定义并不是那么简单,但这正是归纳族在依赖类型语言中所具有的力量 - 它们引入的价值取决于它们的类型和类型,这取决于它们的价值。
我们可以通过查看其定义来检查Fin
给出该属性的内容。
data Fin : Nat -> Set where
zerof : (n : Nat) -> Fin (succ n)
succf : (n : Nat) -> (i : Fin n) -> Fin (succ n)
这需要花一些时间来理解,所以作为一个例子,我们可以尝试构建Fin 2
类型的值。有几种方法可以做到这一点(事实上,我们发现确实有2个)
zerof 1 : Fin 2
zerof 2 : Fin 3 -- nope!
zerof 0 : Fin 1 -- nope!
succf 1 (zerof 0) : Fin 2
这让我们看到有两个居民,并且还演示了一些类型计算的发生方式。特别是,(n : Nat)
类型中的zerof
位将实际的值 n
反映到类型中,允许我们为任何类型形成Fin (n+1)
n : Nat
。之后,我们使用succf
的重复应用程序将Fin
值增加到正确的类型族索引(索引Fin
的自然数)。
提供这些能力的是什么?诚然,在依赖类型的归纳家族和常规Haskell ADT之间存在许多差异,但我们可以专注于与理解GADT最相关的那个。
在GADT和归纳族中,您有机会指定构造函数的精确类型。这可能很无聊
data Nat where
Zero :: Nat
Succ :: Nat -> Nat
或者,如果我们有一个更灵活的索引类型,我们可以选择不同的,更有趣的返回类型
data Typed t where
TyInt :: Int -> Typed Int
TyChar :: Char -> Typed Char
TyUnit :: Typed ()
TyProd :: Typed a -> Typed b -> Typed (a, b)
...
特别是,我们滥用了根据所使用的特定值构造函数修改返回类型的功能。这允许我们将一些值信息反映到类型中,并生成更精细的(纤维)类型。
那么我们可以用它们做什么呢?好吧,有了一点肘部油脂,我们可以produce Fin
in Haskell。简而言之,它要求我们在类型中定义自然概念
data Z
data S a = S a
> undefined :: S (S (S Z)) -- 3
...然后GADT将价值反映到这些类型......
data Nat where
Zero :: Nat Z
Succ :: Nat n -> Nat (S n)
...然后我们可以使用这些来构建Fin
,就像我们在Agda中所做的那样......
data Fin n where
ZeroF :: Nat n -> Fin (S n)
SuccF :: Nat n -> Fin n -> Fin (S n)
最后我们可以构造两个Fin (S (S Z))
*Fin> :t ZeroF (Succ Zero)
ZeroF (Succ Zero) :: Fin (S (S Z))
*Fin> :t SuccF (Succ Zero) (ZeroF Zero)
SuccF (Succ Zero) (ZeroF Zero) :: Fin (S (S Z))
但请注意,我们在归纳家庭中失去了很多便利。例如,我们不能在我们的类型中使用常规数字文字(虽然这在技术上只是Agda中的一个技巧),我们需要创建一个单独的&#34;类型nat&#34;和&#34;值nat&#34;并使用GADT将它们连接在一起,我们也及时发现,虽然类型级数学在Agda中很痛苦,但它可以完成。在哈斯克尔,它非常痛苦,往往不能。
例如,可以在Agda weaken
类型
Fin
概念
weaken : (n <= m) -> Fin n -> Fin m
weaken = ...
我们提供了一个非常有趣的第一个值,n <= m
允许我们嵌入&#34;值小于n
&#34;进入小于m
&#34;的&#34;值集合。从技术上讲,我们可以在Haskell中做同样的事情,但它需要严重滥用类型类prolog。
因此,GADT与依赖类型语言中的归纳家族相似,这种语言较弱且较为笨拙。为什么我们首先要在Haskell中使用它们?
基本上是因为并非所有类型不变量都需要感应族的全部功能来表达,而GADT在表达性,Haskell中的可实现性和类型推断之间选择了特定的折衷方案。
有用的GADT表达式的一些示例是Red-Black Trees which cannot have the Red-Black property invalidated或simply-typed lambda calculus embedded as HOAS piggy-backing off the Haskell type system。
在实践中,您还经常看到GADT用于隐含的存在上下文。例如,类型
data Foo where
Bar :: a -> Foo
使用存在量化隐式隐藏a
类型变量
> :t Bar 4 :: Foo
以某种方式有时很方便。如果仔细观察,维基百科的HOAS示例将其用于a
构造函数中的App
类型参数。在没有GADT的情况下表达该陈述将是一堆存在的语境,但GADT语法使其自然。
答案 1 :(得分:24)
GADT可以为您提供比常规ADT更强的类型强制保证。例如,您可以强制在类型系统级别上平衡二叉树,例如this implementation的2-3 trees:
{-# LANGUAGE GADTs #-}
data Zero
data Succ s = Succ s
data Node s a where
Leaf2 :: a -> Node Zero a
Leaf3 :: a -> a -> Node Zero a
Node2 :: Node s a -> a -> Node s a -> Node (Succ s) a
Node3 :: Node s a -> a -> Node s a -> a -> Node s a -> Node (Succ s) a
每个节点都有一个类型编码的深度,其中所有叶子都驻留在其中。那么一棵树 可以是空树,单例值或未指定深度的节点 使用GADT。
data BTree a where
Root0 :: BTree a
Root1 :: a -> BTree a
RootN :: Node s a -> BTree a
类型系统保证您只能构建平衡节点。
这意味着在这些树上实施insert
等操作时,您的
代码类型 - 只检查其结果是否始终是平衡树。
答案 2 :(得分:15)
我发现“提示”monad(来自“MonadPrompt”软件包)在几个地方都是一个非常有用的工具(以及来自“操作”软件包的等效“Program”monad。与GADTs结合使用(这是怎么回事)本来打算使用它,它允许你非常便宜且非常灵活地制作嵌入式语言。在Monad Reader issue 15中有一篇非常好的文章叫做“三个Monad中的冒险”,它很好地介绍了Prompt monad沿着有一些现实的GADT。
答案 3 :(得分:3)
我喜欢the GHC manual中的示例。这是GADT核心理念的快速演示:您可以将您正在操作的语言的类型系统嵌入到Haskell的类型系统中。这让你的Haskell函数假定并强制它们保留,语法树对应于类型很好的程序。
当我们定义Term
时,我们选择的类型无关紧要。我们可以写
data Term a where
...
IsZero :: Term Char -> Term Char
或
...
IsZero :: Term a -> Term b
并且Term
的定义仍将继续。
只有当我们想计算 Term
时,例如在定义eval
时,类型才会重要。我们需要
...
IsZero :: Term Int -> Term Bool
因为我们需要对eval
进行递归调用才能返回Int
,我们希望返回Bool
。
答案 4 :(得分:2)
这是一个简短的答案,但请咨询Haskell Wikibook。它引导你通过一个良好类型的表达式树的GADT,这是一个相当规范的例子:http://en.wikibooks.org/wiki/Haskell/GADT
GADT还用于实现类型相等:http://hackage.haskell.org/package/type-equality。我找不到合适的论文来引用这种随便的方法 - 这种技术现在已经很好地融入了民间传说。然而,它在Oleg的打字无标签中很常用。见,例如,关于GADT类型汇编的部分。 http://okmij.org/ftp/tagless-final/#tc-GADT