现在是依赖类型的Haskell吗?
Haskell在某种程度上是一种依赖类型的语言。有一个类型级别数据的概念,现在有了可以更合理地键入类型DataKinds
,并且有一些方法(GADTs
)可以为类型级别数据提供运行时表示。因此,运行时值的值有效地显示在type中,这就是对语言进行依赖类型化的意思。
简单数据类型被提升为种类级别,因此它们包含的值可以在类型中使用。因此,原型的例子
data Nat = Z | S Nat
data Vec :: Nat -> * -> * where
VNil :: Vec Z x
VCons :: x -> Vec n x -> Vec (S n) x
成为可能,有了它,诸如
vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil VNil = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)
很好 请注意,长度n
在该函数中是纯静态的东西,即使输入和输出向量的长度在的执行中不起作用,也要确保其长度相同
vApply
。相比之下,它更棘手(即,不可能)来实现,这使得该函数n
的一个给定的副本x
(这将是pure
对vApply
的<*>
)
vReplicate :: x -> Vec n x
因为了解运行时要制作多少个副本至关重要。输入单例。
data Natty :: Nat -> * where
Zy :: Natty Z
Sy :: Natty n -> Natty (S n)
对于任何可推广的类型,我们都可以构建单身家庭,在提升的类型上建立索引,并在其值的运行时重复中居住。Natty n
是type-level的运行时副本的类型n
:: Nat
。我们现在可以写
vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)
因此,您有一个与运行时值绑定的类型级别值:检查运行时副本可精炼有关该类型级别值的静态知识。即使术语和类型是分开的,我们也可以通过使用单例构造作为一种环氧树脂来在相之间建立键,从而以依存类型的方式工作。与允许使用类型的任意运行时表达式相比,这还有很长的路要走,但这并非没有。
什么讨厌?少了什么东西?
让我们对该技术施加压力,看看是什么开始动摇。我们可能会想到单身人士应该更隐性地得到管理
class Nattily (n :: Nat) where
natty :: Natty n
instance Nattily Z where
natty = Zy
instance Nattily n => Nattily (S n) where
natty = Sy natty
让我们写,例如,
instance Nattily n => Applicative (Vec n) where
pure = vReplicate natty
(<*>) = vApply
那行得通,但现在意味着我们的原始Nat
类型产生了三份副本:一种,单例家族和单例类。在交换明确的Natty n
价值和Nattily n
字典方面,我们有一个相当笨拙的过程。而且,Natty
不是Nat
:我们对运行时值有某种依赖性,但与我们最初想到的类型无关。没有完全依赖类型的语言会使依赖类型变得如此复杂!
同时,虽然Nat
可以提升,Vec
但不能。您无法按索引类型进行索引。完全依靠依存类型的语言没有施加任何限制,在我作为依存类型展示的职业中,我学会了在演讲中包含两层索引的示例,目的只是教那些进行过一层索引的人们很难但不可能不要期望我像纸牌屋一样折叠起来。有什么问题?平等。当您将构造函数的特定返回类型赋予显式方程式要求时,GADT会通过将隐式实现的约束转换为隐式工作。像这样。
data Vec (n :: Nat) (x :: *)
= n ~ Z => VNil
| forall m. n ~ S m => VCons x (Vec m x)
在我们两个等式的每一个中,双方都有好感Nat
。
现在,对在向量上建立索引的内容尝试相同的翻译。
data InVec :: x -> Vec n x -> * where
Here :: InVec z (VCons z zs)
After :: InVec z ys -> InVec z (VCons y ys)
变成
data InVec (a :: x) (as :: Vec n x)
= forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
| forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)
现在我们之间形成等式约束as :: Vec n x
,并
VCons z zs :: Vec (S m) x
在双方有语法不同(但可证明等)三种。GHC核心目前尚未配备这种概念!
还缺少什么?好吧,大多数Haskell在类型级别上都是缺失的。实际上,您可以提升的术语语言只有变量和非GADT构造函数。一旦有了这些,type family
机器就可以让您编写类型级别的程序:其中一些可能很像您会考虑在术语级别编写的函数(例如,配备Nat
加法,因此您可以提供一个好的类型来追加Vec
) ,但这只是一个巧合!
实际上,缺少的另一件事是一个库,它利用我们的新功能按值对类型进行索引。在这个勇敢的新世界里做什么Functor
和Monad
成为什么?我正在考虑,但是还有很多事情要做。
运行类型级程序
与大多数依赖类型的编程语言一样,Haskell具有两种
操作语义。运行时系统有一种运行程序的方式(仅在类型擦除后关闭表达式,高度优化),然后还有类型检查器运行程序的方式(您的类型家族,即带有开放表达式的“类型类Prolog”)。对于Haskell,通常不会将两者混为一谈,因为正在执行的程序使用不同的语言。依赖类型的语言针对相同的程序语言具有单独的运行时和静态执行模型,但是不用担心,运行时模型仍然可以让您进行类型擦除,甚至可以进行证明擦除:这就是Coq 提取的内容机制给你;至少这是Edwin Brady的编译器所做的(尽管Edwin删除了不必要的重复值以及类型和证明)。阶段区分可能不再是句法范畴的区分
,但它仍然存在。
总体而言,依赖类型的语言使类型检查器可以运行程序,而不必担心会比长时间等待更糟。随着Haskell的依赖类型越来越多,我们面临的问题是它的静态执行模型应该是什么?一种方法可能是将静态执行限制为全部功能,这将使我们有相同的运行自由度,但可能迫使我们在数据和协同数据之间进行区分(至少对于类型级代码而言),以便我们可以判断是否强制终止或提高生产力。但这不是唯一的方法。我们可以自由选择一个较弱的执行模型,该模型不愿运行程序,其代价是仅通过计算即可得出更少的方程式。实际上,GHC就是这样做的。GHC核心的输入规则没有提及运行
程序,但仅用于检查方程式的证据。当转换为内核时,GHC的约束求解器将尝试运行您的类型级别的程序,并生成一些银色的痕迹,证明给定表达式等于其正常形式。这种证据生成方法有点不可预测,并且不可避免地是不完整的:例如,它与看上去令人毛骨悚然的递归作斗争,这也许是明智的。我们不需要担心的一件事是在类型检查器中执行IO
计算:记住,类型检查器不必具有
launchMissiles
与运行时系统相同的含义!
欣德利·米尔纳文化
Hindley-Milner类型系统实现了四个截然不同的区别的真正令人敬畏的巧合,但不幸的是,文化上的副作用是许多人看不到这些区别之间的区别,并认为巧合是不可避免的!我在说什么
- 术语与类型
- 显式写作与隐式写作
- 出现在运行时VS运行时间之前擦除
- 非依赖抽象与依赖量化
我们习惯于写术语并推断类型,然后再删除它们。我们习惯于对类型变量进行量化,并以静默方式和静态方式进行相应的类型抽象和应用。
在这些区别脱颖而出之前,您不必太偏离香草Hindley-Milner,这并不是一件坏事。首先,如果我们愿意在一些地方编写它们,我们可以有更多有趣的类型。同时,当我们使用重载函数时,我们不必编写类型类字典,但是这些字典肯定在运行时存在(或内联)。在依赖类型的语言中,我们希望在运行时不仅擦除类型,而且(与类型类一样)某些隐式推断的值将不会被擦除。例如,vReplicate
通常可以从所需向量的类型中推断出其数字参数,但我们仍然需要在运行时知道它。
由于这些巧合不再成立,我们应该回顾哪些语言设计选择?例如,Haskell没有提供forall x. t
明确实例化量词的方法对吗?如果类型检查器无法x
通过统一进行猜测,那么t
我们没有其他方法可以说出x
必须是什么。
更广泛地讲,我们不能将“类型推论”视为一个整体概念,我们要么拥有全部,要么一无所有。首先,我们需要将“一般化”方面(Milner的“ let”规则)分开,该方面严重依赖于限制存在的类型,以确保愚蠢的机器可以从“专业化”方面(Milner的“ var ”规则),它与您的约束求解器一样有效。我们可以预期,顶级类型将更难推断,但是内部类型信息将保持相当容易的传播。
Haskell的后续步骤
我们看到类型和种类级别增长非常相似(它们已经在GHC中共享内部表示形式)。我们不妨将它们合并。* :: *
如果可以的话,这会很有趣:我们很早以前就失去了
逻辑上的稳健性,当时我们允许使用底部,但是类型
稳健性通常是较弱的要求。我们必须检查。如果我们必须具有不同的类型,种类等级别,则至少可以确保始终可以促进类型级别及更高级别的所有内容。只需重用我们已经为类型使用的多态性,而不是在种类级别重新发明多态性。
我们应该通过允许异类方程简化和概括当前的约束系统,这些异类方程a ~ b
的a
和
类型在b
语法上不相同(但可以证明是相等的)。这是一项古老的技术(在我的论文中是上个世纪),它使依赖更加容易应对。我们将能够表达对GADT中表达式的约束,从而放宽对可以推广的内容的限制。
我们应该通过引入从属函数类型来消除对单例构造的需要pi x :: s -> t
。具有这种类型的函数可以显式地应用于s
存在于类型和术语语言的交集中的任何类型的表达式(因此,变量,构造函数以及更多稍后介绍)。相应的lambda和应用程序不会在运行时删除,因此我们可以编写
vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)
无需更换Nat
的Natty
。的域pi
可以是任何可推广的类型,因此,如果可以推广GADT,我们可以编写依赖的量词序列(或如de Briuijn所称的“望远镜”)
pi n :: Nat -> pi xs :: Vec n x -> ...
到我们需要的长度。
这些步骤的目的是通过直接使用更通用的工具来消除复杂性,而不是使用较弱的工具和笨拙的编码。当前的部分买入使Haskell的依存类型的好处比它们需要的更为昂贵。
太难?
依赖类型使很多人感到紧张。它们使我紧张,但是我喜欢紧张,或者至少我很难不感到紧张。但是,围绕该主题产生如此众多的无知并没有帮助。部分原因是由于我们还有很多东西要学习。但是,众所周知,不太激进的方法的支持者会引起对依存类型的恐惧,而不必始终确保事实完全依赖于依存类型。我不会命名。这些“不确定的类型检查”,“转弯不完整”,“无相位区分”,“无类型擦除”,“无处不在的证明”等等,即使它们是垃圾,神话仍然存在。
当然,依赖类型的程序必须始终被证明是正确的并非绝对如此。可以改善程序的基本卫生状况,在不完全遵循完整规范的情况下强制执行其他类型的不变量。朝这个方向迈出的小步通常会产生更强大的担保,而几乎没有或没有额外的举证责任。依赖类型的程序不可避免地充满了证明是不正确的,的确,我通常以代码中存在任何证明作为质疑我的定义的线索。
因为,正如在表达能力上的任何提高一样,我们可以自由地说坏话和公平话。例如,有很多定义二进制搜索树的方法,但这并不意味着没有一个好的方法。重要的是,不要假定不良经历无法改善,即使它会使自我屈服。设计依赖定义是一项需要学习的新技能,而成为Haskell程序员并不会自动使您成为专家!即使某些程序犯规,为什么还要剥夺其他程序公平的自由?
为什么仍然困扰Haskell?
我真的很喜欢依赖类型,但是我大多数的黑客项目仍然在Haskell中。为什么?Haskell具有类型类。Haskell有有用的库。Haskell对效果编程有一种可行的(尽管远非理想)处理。Haskell具有工业实力的编译器。依赖类型的语言处于社区和基础设施发展的更早期阶段,但是我们将到达那里,并且通过诸如元编程和数据类型泛型的方式在世代上发生了真正的转变。但是,您只需要查看一下Haskell向依赖类型迈出的步伐,人们在做什么,就可以看到通过推动当前一代语言的发展也可以获得很多好处。