GADT 的实际使用

发布于 2024-09-26 01:42:45 字数 158 浏览 4 评论 0原文

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

haskell wikibook 中给出的示例太短,无法让我了解真实情况GADT 的可能性。

How do I make use of Generalized Algebraic Data Type?

The example given in the haskell wikibook is too short to give me an insight of the real possibilities of GADT.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

活雷疯 2024-10-03 01:42:46

与常规 ADT 相比,GADT 可以为您提供更强的类型强制保证。例如,您可以强制二叉树在类型系统级别上平衡,如 此实现 2-3 棵树

{-# 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 之类的操作时,您的
仅当其结果始终是平衡树时,代码才进行类型检查。

GADTs can give you stronger type enforced guarantees than regular ADTs. For example, you can force a binary tree to be balanced on the type system level, like in this implementation of 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

Each node has a type-encoded depth where all its leaves reside. A tree is then
either an empty tree, a singleton value, or a node of unspecified depth, again
using GADTs.

data BTree a where
    Root0 :: BTree a
    Root1 :: a -> BTree a
    RootN :: Node s a -> BTree a

The type system guarantees you that only balanced nodes can be constructed.
This means that when implementing operations like insert on such trees, your
code type-checks only if its result is always a balanced tree.

我们的影子 2024-10-03 01:42:46

我发现“Prompt”monad(来自“MonadPrompt”包)在几个地方都是一个非常有用的工具(以及“Operational”包中的等效“Program”monad)。与 GADT 结合(这就是它的目的)使用),它允许您非常便宜且非常灵活地制作嵌入式语言Monad Reader 第 15 期 称为“三个 Monad 的冒险”,其中对 Prompt monad 以及一些现实的 GADT 进行了很好的介绍。

I have found the "Prompt" monad (from the "MonadPrompt" package) a very useful tool in several places (along with the equivalent "Program" monad from the "operational" package. Combined with GADTs (which is how it was intended to be used), it allows you to make embedded languages very cheaply and very flexibly. There was a pretty good article in the Monad Reader issue 15 called "Adventures in Three Monads" that had a good introduction to the Prompt monad along with some realistic GADTs.

天涯离梦残月幽梦 2024-10-03 01:42:46

我喜欢 GHC 中的示例手册。这是 GADT 核心思想的快速演示:您可以将正在操作的语言的类型系统嵌入到 Haskell 的类型系统中。这让你的 Haskell 函数假设并强制它们保留语法树对应于类型良好的程序。

当我们定义Term时,我们选择什么类型并不重要。我们可以写

data Term a where
  ...
  IsZero :: Term Char -> Term Char

or

  ...
  IsZero :: Term a -> Term b

并且 Term 的定义仍然会进行。

只有当我们想要计算Term时,例如在定义eval时,类型才重要。我们需要这样做,

  ...
  IsZero :: Term Int -> Term Bool

因为我们需要递归调用 eval 来返回 Int,并且我们希望依次返回 Bool

I like the example in the GHC manual. It's a quick demo of a core GADT idea: that you can embed the type system of a language you're manipulating into Haskell's type system. This lets your Haskell functions assume, and forces them to preserve, that the syntax trees correspond to well-typed programs.

When we define Term, it doesn't matter what types we choose. We could write

data Term a where
  ...
  IsZero :: Term Char -> Term Char

or

  ...
  IsZero :: Term a -> Term b

and the definition of Term would still go through.

It's only once we want to compute on Term, such as in defining eval, that the types matter. We need to have

  ...
  IsZero :: Term Int -> Term Bool

because we need our recursive call to eval to return an Int, and we want to in turn return a Bool.

这是一个简短的答案,但请参阅 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

This is a short answer, but consult the Haskell Wikibook. It walks you though a GADT for a well-typed expression tree, which is a fairly canonical example: http://en.wikibooks.org/wiki/Haskell/GADT

GADTs are also used for implementing type equality: http://hackage.haskell.org/package/type-equality. I can't find the right paper to reference for this offhand -- this technique has made its way well into folklore by now. It is used quite well, however, in Oleg's typed tagless stuff. See, e.g. the section on typed compilation into GADTs. http://okmij.org/ftp/tagless-final/#tc-GADT

妄想挽回 2024-10-03 01:42:45

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

这让我们看到有两个居民,并且还演示了类型计算是如何发生的。特别是,zerof 类型中的 (n : Nat) 位反映了实际 n该类型允许我们为任何 n : Nat 形成 Fin (n+1)。之后,我们重复应用 succfFin 值增加到正确的类型族索引(索引 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)
  ...

特别是,我们滥用了根据特定值修改返回类型的能力使用的构造函数。这使我们能够将一些值信息反映到类型中并生成更精细指定(纤维化)的类型。


那么我们能用它们做什么呢?好吧,通过一点点苦劳,我们可以 在 Haskell 中生成 Fin。简而言之,它要求我们在类型中定义自然数的概念

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 (SZ)) 值

*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 中的一个技巧),我们需要创建一个单独的“type nat”和“value nat”并使用 GADT 将它们链接在一起,随着时间的推移,我们还会发现,虽然类型级数学在 Agda 中很痛苦,但它是可以完成的。在 Haskell 中,这是非常痛苦的,而且通常不能。

例如,可以在 Agda 的 Fin 类型中定义一个 weaken 概念

weaken : (n <= m) -> Fin n -> Fin m
weaken = ...

,其中我们提供一个非常有趣的第一个值,即 n <= mn <= m code> 允许我们将“小于 n 的值”嵌入到“小于 m 的值”集合中。从技术上讲,我们可以在 Haskell 中做同样的事情,但它需要大量滥用类型类序言。


因此,GADT 类似于依赖类型语言中的归纳族,但更弱且更笨拙。为什么我们首先要在 Haskell 中使用它们?

基本上是因为并非所有类型不变量都需要归纳族的全部功能来表达,并且 GADT 在表达性、Haskell 中的可实现性和类型推断之间选择特定的折衷方案。

有用的 GADT 表达式的一些示例是 不能具有红黑关系的红黑树属性无效嵌入为 HOAS 搭载 Haskell 的简单类型 lambda 演算类型系统

在实践中,您还经常看到 GADT 用于隐式存在上下文。例如,该类型

data Foo where
  Bar :: a -> Foo

隐式隐藏 a 类型变量

> :t Bar 4 :: Foo

使用存在量化以有时方便的方式 。如果您仔细查看,维基百科中的 HOAS 示例将其用作 App 构造函数中的 a 类型参数。在没有 GADT 的情况下表达该语句将是存在上下文的混乱,但 GADT 语法使其变得自然。

GADTs are weak approximations of inductive families from dependently typed languages—so let's begin there instead.

Inductive families are the core datatype introduction method in a dependently typed language. For instance, in Agda you define the natural numbers like this

data Nat : Set where
  zero : Nat
  succ : Nat -> Nat 

which isn't very fancy, it's essentially just the same thing as the Haskell definition

data Nat = Zero | Succ Nat

and indeed in GADT syntax the Haskell form is even more similar

{-# LANGUAGE GADTs #-}

data Nat where
  Zero :: Nat
  Succ :: Nat -> Nat

So, at first blush you might think GADTs are just neat extra syntax. That's just the very tip of the iceberg though.


Agda has capacity to represent all kinds of types unfamiliar and strange to a Haskell programmer. A simple one is the type of finite sets. This type is written like Fin 3 and represents the set of numbers {0, 1, 2}. Likewise, Fin 5 represents the set of numbers {0,1,2,3,4}.

This should be quite bizarre at this point. First, we're referring to a type which has a regular number as a "type" parameter. Second, it's not clear what it means for Fin n to represent the set {0,1...n}. In real Agda we'd do something more powerful, but it suffices to say that we can define a contains function

contains : Nat -> Fin n -> Bool
contains i f = ?

Now this is strange again because the "natural" definition of contains would be something like i < n, but n is a value that only exists in the type Fin n and we shouldn't be able to cross that divide so easily. While it turns out that the definition is not nearly so straightforward, this is exactly the power that inductive families have in dependently typed languages—they introduce values that depend on their types and types that depend on their values.


We can examine what it is about Fin that gives it that property by looking at its definition.

data Fin : Nat -> Set where
  zerof : (n : Nat) -> Fin (succ n)
  succf : (n : Nat) -> (i : Fin n) -> Fin (succ n)

this takes a little work to understand, so as an example lets try constructing a value of the type Fin 2. There are a few ways to do this (in fact, we'll find that there are exactly 2)

zerof 1           : Fin 2
zerof 2           : Fin 3 -- nope!
zerof 0           : Fin 1 -- nope!
succf 1 (zerof 0) : Fin 2

This lets us see that there are two inhabitants and also demonstrates a little bit of how type computation happens. In particular, the (n : Nat) bit in the type of zerof reflects the actual value n up into the type allowing us to form Fin (n+1) for any n : Nat. After that we use repeated applications of succf to increment our Fin values up into the correct type family index (natural number that indexes the Fin).

What provides these abilities? In all honesty there are many differences in between a dependently typed inductive family and a regular Haskell ADT, but we can focus on the exact one that is most relevant to understanding GADTs.

In GADTs and inductive families you get an opportunity to specify the exact type of your constructors. This might be boring

data Nat where
  Zero :: Nat
  Succ :: Nat -> Nat

Or, if we have a more flexible, indexed type we can choose different, more interesting return types

data Typed t where
  TyInt  :: Int                -> Typed Int
  TyChar :: Char               -> Typed Char
  TyUnit ::                       Typed ()
  TyProd :: Typed a -> Typed b -> Typed (a, b)
  ...

In particular, we're abusing the ability to modify the return type based on the particular value constructor used. This allows us to reflect some value information up into the type and produce more finely specified (fibered) typed.


So what can we do with them? Well, with a little bit of elbow grease we can produce Fin in Haskell. Succinctly it requires that we define a notion of naturals in types

data Z
data S a = S a

> undefined :: S (S (S Z))  -- 3

... then a GADT to reflect values up into those types...

data Nat where
  Zero :: Nat Z
  Succ :: Nat n -> Nat (S n)

... then we can use these to build Fin much like we did in Agda...

data Fin n where
  ZeroF :: Nat n -> Fin (S n)
  SuccF :: Nat n -> Fin n -> Fin (S n)

And finally we can construct exactly two values of 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))

But notice that we've lost a lot of convenience over the inductive families. For instance, we can't use regular numeric literals in our types (though that's technically just a trick in Agda anyway), we need to create a separate "type nat" and "value nat" and use the GADT to link them together, and we'd also find, in time, that while type level mathematics is painful in Agda it can be done. In Haskell it's incredibly painful and often cannot.

For instance, it's possible to define a weaken notion in Agda's Fin type

weaken : (n <= m) -> Fin n -> Fin m
weaken = ...

where we provide a very interesting first value, a proof that n <= m which allows us to embed "a value less than n" into the set of "values less than m". We can do the same in Haskell, technically, but it requires heavy abuse of type class prolog.


So, GADTs are a resemblance of inductive families in dependently typed languages that are weaker and clumsier. Why do we want them in Haskell in the first place?

Basically because not all type invariants require the full power of inductive families to express and GADTs pick a particular compromise between expressiveness, implementability in Haskell, and type inference.

Some examples of useful GADTs expressions are Red-Black Trees which cannot have the Red-Black property invalidated or simply-typed lambda calculus embedded as HOAS piggy-backing off the Haskell type system.

In practice, you also often see GADTs use for their implicit existential context. For instance, the type

data Foo where
  Bar :: a -> Foo

implicitly hides the a type variable using existential quantification

> :t Bar 4 :: Foo

in a way that is sometimes convenient. If you look carefully the HOAS example from Wikipedia uses this for the a type parameter in the App constructor. To express that statement without GADTs would be a mess of existential contexts, but the GADT syntax makes it natural.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文