真实世界使用GADT

时间:2010-10-04 21:18:18

标签: haskell gadt

如何使用广义代数数据类型?

haskell wikibook中给出的例子太短,无法让我了解GADT的真正可能性。

5 个答案:

答案 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 = ?

现在这又是奇怪的,因为&#34;自然&#34; 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 invalidatedsimply-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 implementation2-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