为什么不依赖输入?


161

我已经看到一些消息来源回应了这样的观点:“ Haskell正在逐渐成为一种依赖类型的语言”。这似乎意味着,随着越来越多的语言扩展,Haskell朝着这个总体方向发展,但是还不存在。

我基本上想知道两件事。首先,很简单,“成为依赖类型的语言”实际上什么意思?(希望不要太过专业。)

第二个问题是...缺点是什么?我的意思是,人们知道我们正朝着这个方向前进,因此必须有一定的优势。但是,我们还没有到那儿,因此必须存在一些不利因素,阻止人们一路前进。我觉得问题是复杂性急剧增加。但是,我不确定是否真正了解依存类型是什么。

知道的是,每当我开始阅读有关依赖类型的编程语言的内容时,文本都是完全难以理解的……大概就是问题所在。(?)


10
简而言之,您可以编写取决于术语(计算)的类型。这足以指定有关程序各个方面的类型,因此意味着类型系统具有完整的程序规范。问题在于,因为类型取决于计算,所以类型检查要困难得多(通常是不可能的)。
GManNickG 2012年

27
@GManNickG:完全可以进行类型检查。类型推断是另一回事,但是从那以后,GHC的各种扩展再次放弃了应该推断所有类型的想法。
CA McCann 2012年

7
如果我正确理解的话,缺点是正确地进行依赖类型的键入(例如,以既可用又可靠的方式进行)很困难,而且我们还不知道。
即将

1
@CAMcCann:是的,我的错。
GManNickG 2012年

4
我认为没有人指出一个大的实用缺陷:编写证明所有代码都是正确的证明非常烦人。因为您不能自动进行类型推断(对应于“强大的海拉”逻辑中的定理证明),所以您必须以证明形式为程序编写注释。显然,这会令人烦恼并且过一会儿很难做,尤其是对于人们通常在Haskell中使用的更为复杂的单子魔术。这些天来最接近的语言是为我们完成大部分任务或为我们提供大量原始变量的语言。
克里斯托弗·米辛斯基

Answers:


21

依赖类型实际上就是值和类型级别的统一,因此您可以参数化类型的值(在Haskell中已经可以使用类型类和参数多态性进行参数化),并且可以参数化类型的值(严格地说,在Haskell中尚不可能) ,尽管DataKinds距离非常近)。

编辑: 显然,从现在开始,我错了(请参阅@pigworker的评论)。我将保留其余的内容作为我被喂饱的神话的记录。:P


据我所知,转向完全依赖类型的问题在于,它将打破类型和值级别之间的相位限制,从而使Haskell可以编译为具有擦除类型的高效机器代码。在我们目前的技术水平下,依赖类型的语言必须在某个时候通过解释器(或者立即进行编译,或者在编译为依赖类型的字节码或类似代码之后)。

这不一定是基本的限制,但是我个人并不知道任何当前的研究在这方面看似有希望,但尚未纳入GHC。如果其他人知道更多,我很乐意得到纠正。


46
你所说的几乎是完全错误的。我并没有完全责怪您:事实重复了标准的神话。Edwin Brady的语言Idris执行类型擦除(因为没有运行时行为取决于类型),并生成相当标准的Lambda提升的超级组合器编码,并使用常规G机技术从中生成代码。
养猪工人

3
附带说明,最近有人指出我对这篇论文。据我所知,这将使Haskell具有依赖关系的种类(即类型级别的语言将具有依赖关系的类型),这与我看到我们很快就会得到的联系非常接近。
Ptharien的圣火,2012年

8
是的,那篇论文确实展示了如何使类型依赖于类型级别的东西(以及消除了类型/种类的区别)。已经在讨论中的可行的后续措施是允许实际的依赖函数类型,但将其参数限制为可以同时存在于值层和类型层中的语言片段(由于数据类型的提升,现在不平凡)。那将消除对单例构造的需要,该构造目前使“伪造”比期望的更为复杂。我们正在逐步接近真实事物。
猪场工人

13
有许多实用的问题,将依赖类型改写为Haskell。一旦有了这种受限形式的依存函数空间形式,我们仍然面临着一个问题,即如何扩大类型级别所允许的值语言的片段,以及其等式理论应该是什么(就像我们想要2 + 2是4,等等)。从零开始,从零开始依赖设计的语言有很多杂乱无章的问题(例如,底部)。
猪场工人

2
@pigworker总有Haskell的干净子集吗?如果是这样,我们是否不能仅将它用于“可以同时存在于值和类型层中的语言片段”?如果没有,生产一个会怎样?
Ptharien的圣火,2012年

223

现在是依赖类型的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(这将是purevApply<*>

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) ,但这只是一个巧合!

实际上,缺少的另一件事是一个,它利用我们的新功能按值对类型进行索引。在这个勇敢的新世界里做什么FunctorMonad成为什么?我正在考虑,但是还有很多事情要做。

运行类型级程序

与大多数依赖类型的编程语言一样,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 ~ ba和 类型在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)

无需更换NatNatty。的域pi可以是任何可推广的类型,因此,如果可以推广GADT,我们可以编写依赖的量词序列(或如de Briuijn所称的“望远镜”)

pi n :: Nat -> pi xs :: Vec n x -> ...

到我们需要的长度。

这些步骤的目的是通过直接使用更通用的工具来消除复杂性,而不是使用较弱的工具和笨拙的编码。当前的部分买入使Haskell的依存类型的好处比它们需要的更为昂贵。

太难?

依赖类型使很多人感到紧张。它们使我紧张,但是我喜欢紧张,或者至少我很难不感到紧张。但是,围绕该主题产生如此众多的无知并没有帮助。部分原因是由于我们还有很多东西要学习。但是,众所周知,不太激进的方法的支持者会引起对依存类型的恐惧,而不必始终确保事实完全依赖于依存类型。我不会命名。这些“不确定的类型检查”,“转弯不完整”,“无相位区分”,“无类型擦除”,“无处不在的证明”等等,即使它们是垃圾,神话仍然存在。

当然,依赖类型的程序必须始终被证明是正确的并非绝对如此。可以改善程序的基本卫生状况,在不完全遵循完整规范的情况下强制执行其他类型的不变量。朝这个方向迈出的小步通常会产生更强大的担保,而几乎没有或没有额外的举证责任。依赖类型的程序不可避免地充满了证明是不正确的,的确,我通常以代码中存在任何证明作为质疑我的定义的线索。

因为,正如在表达能力上的任何提高一样,我们可以自由地说坏话和公平话。例如,有很多定义二进制搜索树的方法,但这并不意味着没有一个好的方法。重要的是,不要假定不良经历无法改善,即使它会使自我屈服。设计依赖定义是一项需要学习的新技能,而成为Haskell程序员并不会自动使您成为专家!即使某些程序犯规,为什么还要剥夺其他程序公平的自由?

为什么仍然困扰Haskell?

我真的很喜欢依赖类型,但是我大多数的黑客项目仍然在Haskell中。为什么?Haskell具有类型类。Haskell有有用的库。Haskell对效果编程有一种可行的(尽管远非理想)处理。Haskell具有工业实力的编译器。依赖类型的语言处于社区和基础设施发展的更早期阶段,但是我们将到达那里,并且通过诸如元编程和数据类型泛型的方式在世代上发生了真正的转变。但是,您只需要查看一下Haskell向依赖类型迈出的步伐,人们在做什么,就可以看到通过推动当前一代语言的发展也可以获得很多好处。


6
我真的不在乎DataKinds的东西。主要是因为我想做这样的事情:fmap read getLine >>= \n -> vReplicate n 0。正如您所指出的,这Natty是一种远离此的方法。此外,vReplicate应该可以转换为实际的内存阵列,例如newtype SVector n x = SVector (Data.Vector.Vector x),其中n具有种类Nat(或类似种类)。也许是“依赖型炫耀”的另一个示范点?
约翰L

7
您能说说您对效果编程的理想处理的想法吗?
史蒂文·肖

6
感谢您的出色写作。我很乐意看到一些依赖类型代码的示例,其中某些数据源自程序外部(例如,从文件中读取),以了解在这种情况下如何将值提升为类型。我有一种感觉,所有示例都涉及具有静态已知大小的向量(实现为列表)。
tibbe 2012年

4
@pigworker您将“无相位区分”作为神话(我同意的其他神话)。但是您还没有在我见过的论文和演讲中拆除过这个,同时我尊敬的另一个人告诉我:“依赖类型理论不同于典型的编译器,因为我们不能有意义地将类型检查,编译和执行阶段分开。 ” (请参阅Andrej在2012年11月8日发布的最新帖子)以我的“伪造”经验,有时有时至少会使相位区分模糊,尽管不必擦除它。您能在这个问题上扩大吗?
sclv 2012年

4
@sclv我的作品并没有特别针对“没有阶段区分”的神话,但其他人的目标却是。我建议詹姆斯·麦金纳(James McKinna)和埃德温·布雷迪(Edwin Brady)所拒绝的“ Epigram汇编中的阶段性区别”是一个很好的起点。但是,也请参阅Coq中有关程序提取的更老的工作。由类型检查器完成的开放术语评估与通过提取到ML的执行完全分开,并且很明显,提取会剥离类型和证明。
养猪工人

20

John,这是对依赖类型的另一个常见误解:当数据仅在运行时可用时,它们将不起作用。这是执行getLine示例的方法:

data Some :: (k -> *) -> * where
  Like :: p x -> Some p

fromInt :: Int -> Some Natty
fromInt 0 = Like Zy
fromInt n = case fromInt (n - 1) of
  Like n -> Like (Sy n)

withZeroes :: (forall n. Vec n Int -> IO a) -> IO a
withZeroes k = do
  Like n <- fmap (fromInt . read) getLine
  k (vReplicate n 0)

*Main> withZeroes print
5
VCons 0 (VCons 0 (VCons 0 (VCons 0 (VCons 0 VNil))))

编辑:嗯,那应该是对养猪工人答案的评论。我显然失败了。


您的第一句话似乎有些奇怪;我想说的依赖类型的一点是,他们的工作时,数据仅可在运行时。但是,这种CPS风格的技术并不相同。假设您有一个功能Vec Zy -> IO String。您不能将其与一起使用withZeroes,因为类型Zy不能与forall n统一。也许您可以针对一两个特殊情况解决此问题,但很快就会失去控制。
约翰L

当采用简单类型的值(例如getLine中的String)并将其转换为具有更强类型的值(例如上述的Natty n)时,关键是必须让类型检查器确信您正在执行必要的动态检查。在您的示例中,您正在读取任意数字,因此forall n很有意义。可以以相同方式实施更精确的限制。您有比以下更好的示例吗Vec Zy(该程序仍需要处理用户输入5而不是0)?
ulfnorell

1
我想用第一句话说的是,我偶尔会遇到一些人,他们认为,如果通过与外界的交互来获取数据,就不能使用依赖类型。我的观点是,您要做的唯一一件事就是编写一个依赖类型的解析器,该解析器通常很简单。
ulfnorell

1
ulfnorell:对不起,我不清楚。假设您有一个将与一起使用的函数,Vec Zy -> IO String而另一个Vec n -> IO String与一起使用,并且您只想在类型匹配时才使用第一个函数。是的,这是可能的,但是启用它的机制很笨拙。这是非常简单的逻辑;如果您有更复杂的逻辑,那就更糟了。另外,您可能需要在CPS中重新编写很多代码。而且您仍然没有依赖于值级别术语的类型级别表达式
John L

啊,我明白你在说什么。这就是Natty的用途,例如在vReplicate中,我们根据n进行不同的操作。确实,这可能会有些笨拙。CPS风格的替代方法是使用打包的存在物:zeroes :: IO (Some (Flip Vec Int))
ulfnorell

19

养猪工人很好地讨论了为什么我们应该走向依赖类型:(a)它们很棒。(b)他们实际上将简化 Haskell已经做的很多事情。

至于“为什么不呢?” 问题,我认为有几点。第一点是,尽管依赖类型背后的基本概念很容易(允许类型依赖于值),但该基本概念的分支既微妙又深刻。例如,值和类型之间的区分仍然有效。但是讨论它们之间的区别变得很远比yer Hindley-Milner或System F更加细微。在某种程度上,这是由于从属类型从根本上来说是很难的(例如,一阶逻辑是不可确定的)。但是我认为更大的问题确实是我们缺乏捕捉和解释正在发生的事情的良好词汇。随着越来越多的人学习依赖类型,我们将开发出更好的词汇表,因此即使基本问题仍然很棘手,事情也将变得更容易理解。

第二点与Haskell 成长的事实有关对依赖类型。因为我们正在朝着该目标逐步发展,但实际上并未实现,所以我们坚持使用在增量补丁之上具有增量补丁的语言。随着新思想的流行,其他语言也发生了同样的事情。Java过去没有具有(参数)多态性。当他们最终添加它时,它显然是一个渐进的改进,其中包含一些抽象漏洞和削弱的功能。事实证明,混合子类型和多态性本来就很难。但这不是Java泛型如此工作的原因。由于必须对Java的较旧版本进行增量改进,因此它们按自己的方式工作。同上,追溯到OOP发明的那一天,人们开始写“目标” C(不要与Objective-C混淆),等等。请记住,C ++最初是以C的严格超集为幌子的。添加新范例总是需要重新定义语言,否则最终会导致一些复杂的混乱。我所有这些的观点是,向Haskell添加真正的依赖类型将需要一定程度的胆量和结构化语言-如果我们要正确的话。但是,要进行这种大修确实很困难,而我们一直在取得的渐进式进展在短期内似乎便宜。确实,黑客攻击GHC的人并不多,但是有很多遗留代码可以存活。这就是为什么如此之多的衍生语言(例如DDC,Cayenne,Idris等)的部分原因。C ++是以C的严格超集为幌子的。添加新的范例总是需要重新定义语言,否则最终会导致一些复杂的混乱。我所有这些的观点是,向Haskell添加真正的依赖类型将需要一定程度的胆量和结构化语言-如果我们要正确的话。但是,要进行这种大修确实很困难,而我们一直在取得的渐进式进展在短期内似乎便宜。确实,黑客攻击GHC的人并不多,但是有很多遗留代码可以存活。这就是为什么如此之多的衍生语言(例如DDC,Cayenne,Idris等)的部分原因。C ++是以C的严格超集为幌子的。添加新的范例总是需要重新定义语言,否则最终会导致一些复杂的混乱。我所有这些的观点是,向Haskell添加真正的依赖类型将需要一定程度的胆量和结构化语言-如果我们要正确的话。但是,要进行这种大修确实很困难,而我们一直在取得的渐进式进展在短期内似乎便宜。确实,黑客攻击GHC的人并不多,但是有很多遗留代码可以存活。这就是为什么如此之多的衍生语言(例如DDC,Cayenne,Idris等)的部分原因。否则最终会导致一些复杂的混乱。我所有这些的观点是,向Haskell添加真正的依赖类型将需要一定程度的胆量和结构化语言-如果我们要正确的话。但是,要进行这种大修确实很困难,而我们一直在取得的渐进式进展在短期内似乎便宜。确实,黑客攻击GHC的人并不多,但是有很多遗留代码可以存活。这就是为什么如此之多的衍生语言(例如DDC,Cayenne,Idris等)的部分原因。否则最终会导致一些复杂的混乱。我所有这些的观点是,向Haskell添加真正的依赖类型将需要一定程度的胆量和结构化语言-如果我们要正确的话。但是,要进行这种大修确实很困难,而我们一直在取得的渐进式进展在短期内似乎便宜。确实,黑客攻击GHC的人并不多,但是有很多遗留代码可以存活。这就是为什么如此之多的衍生语言(例如DDC,Cayenne,Idris等)的部分原因。进行此类大修确实非常困难,而短期内我们一直在取得的增量进展似乎便宜。确实,黑客攻击GHC的人并不多,但是有很多遗留代码可以存活。这就是为什么如此之多的衍生语言(例如DDC,Cayenne,Idris等)的部分原因。进行此类大修确实非常困难,而短期内我们一直在取得的增量进展似乎便宜。确实,黑客攻击GHC的人并不多,但是有很多遗留代码可以存活。这就是为什么如此之多的衍生语言(例如DDC,Cayenne,Idris等)的部分原因。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.