Haskell单身人士:我们用SNat获得了什么

时间:2017-07-21 10:47:39

标签: haskell dependent-type type-level-computation singleton-type

我试图惹恼Haskell单身人士。

在论文Dependently Typed Programming with Singletons中 并在他的博客文章singletons v0.9 Released! Richard Eisenberg定义了数据类型 Nat ,用peano公理定义了自然数:

data Nat = Zero | Succ Nat

通过使用语言扩展 DataKinds ,此数据类型将提升为类型级别。 数据构造器 Zero Succ 被提升为类型构造函数' Zero ' Succ 。 有了这个,我们得到每个自然数在类型级别上的单个和唯一对应类型。例如,对于 3 ,我们得到' Succ(' Succ(' Succ' Zero))。 所以我们现在有自然数字作为类型。

然后,他在值级别定义函数 plus ,在类型级别定义类型族 Plus 有额外的操作。 通过单例库的 promote 函数/ quasiqoter,我们可以自动完成 从 plus 函数创建 Plus 类型系列。所以我们可以避免写自己的类型家庭。

到目前为止一切顺利!

使用GADT语法,他还定义了数据类型 SNat

data SNat :: Nat -> * where
  SZero :: SNat Zero
  SSucc :: SNat n -> SNat (Succ n)

基本上他只将 Nat 类型包装到 SNat 构造函数中。 为什么这有必要?我们获得了什么? 数据类型 Nat SNat 是不同构的吗?为什么 SNat 是单身人士,为什么 Nat 不是 单身人士?在这两种情况下,每种类型都有一个单独的值,即相应的自然数。

1 个答案:

答案 0 :(得分:32)

我们获得了什么?嗯。单身人士的状态是尴尬但当前必要的解决方法,我们越早取消他们就越好。

让我看看我是否可以澄清这张照片。我们有一种数据类型Nat

data Nat = Zero | Suc Nat

(战争已经开始了,甚至比Suc中的&#c; c的数量更加琐碎的问题

类型Nat具有在类型级别无法区分的运行时值。 Haskell类型系统目前具有替换属性,这意味着在任何类型很好的程序中,您可以用具有相同范围和类型的替代子表达式替换任何良好类型的子表达式,并且程序将继续打得好。例如,您可以重写每次出现的

if <b> then <t> else <e>

if <b> then <e> else <t>

你可以确定一切都不会出错......检查你的程序类型的结果。

替换财产是一种尴尬。它清楚地证明了你的类型系统在意义开始重要的那一刻就放弃了。

现在,作为运行时值的数据类型,Nat也成为类型级值'Zero'Suc的一种类型。后者只存在于Haskell的类型语言中,根本没有运行时存在。请注意,尽管类型级别存在'Zero'Suc,但将它们称为&#34;类型&#34;无效。而目前这样做的人应该停止。它们没有类型*,因此不能对值进行分类,这是值得这个名称的类型。

在运行时和类型级Nat之间没有直接的交换方式,这可能会令人讨厌。范例示例涉及向量的关键操作:

data Vec :: Nat -> * -> * where
  VNil   :: Vec 'Zero x
  VCons  :: x -> Vec n x -> Vec ('Suc n) x

我们可能想要计算给定元素的副本向量(可能作为Applicative实例的一部分)。给出类型

似乎是个好主意
vec :: forall (n :: Nat) (x :: *). x -> Vec n x

但这可能有用吗?为了制作某些内容的n副本,我们需要在运行时知道n:程序必须决定是部署VNil还是停止或部署VCons和继续前进,需要一些数据才能做到这一点。一个很好的线索是forall量词,它是参数:它表示量化信息仅适用于类型,并且被运行时删除。

Haskell目前在依赖量化(forall做什么)和运行时擦除之间实施完全虚假的重合。 支持依赖但未删除的量词,我们通常称之为pivec的类型和实现应该类似于

vec :: pi (n :: Nat) -> forall (x :: *). Vec n x
vec 'Zero    x = VNil
vec ('Suc n) x = VCons x (vec n x)

其中pi中的参数 - 位置是用类型语言编写的,但数据在运行时可用。

那我们该做什么呢?我们使用单例来间接捕获类型级数据的运行时副本的含义。

data SNat :: Nat -> * where
  SZero :: SNat Zero
  SSuc  :: SNat n -> SNat (Suc n)

现在,SZeroSSuc生成运行时数据。 SNatNat不同构:前者的类型为Nat -> *,后者的类型为*,因此尝试使它们同构是一种类型错误。 Nat中有许多运行时值,类型系统不区分它们;在每个不同的SNat n中只有一个运行时值(值得一提),因此类型系统无法区分它们的事实就是重点。关键是每个SNat n的每个n是不同的类型,并且GADT模式匹配(其中模式可以是已知匹配的GADT类型的更具体的实例)可以改进我们对n的了解。

我们现在可以写

vec :: forall (n :: Nat). SNat n -> forall (x :: *). x -> Vec n x
vec SZero    x = VNil
vec (SSuc n) x = VCons x (vec n x)

Singletons允许我们通过利用允许细化类型信息的唯一形式的运行时分析来弥补运行时和类型级数据之间的差距。想知道他们是否真的有必要,而且他们现在只是因为这个差距尚未消除,这是非常明智的。