在Haskell社区中,我们正在慢慢添加依赖类型的功能。依赖类型是一种高级类型功能,类型可以依赖于值。像Agda和Idris这样的语言已经拥有它们。它似乎是一个非常高级的功能,需要一个高级类型系统,直到你意识到python 有依赖类型有依赖类型的动态类型,可能是也可能不是实际的依赖类型,从一开始。
对于函数式编程语言中的大多数程序,无论打字有多高级,都有一种方法可以将其作为无类型的lambda演算术语重复出现。这是因为打字只会删除程序,而不会启用新程序。
强力打字赢得了我们的安全。运行时发生的错误类别在运行时不再发生。这种安全性非常好。除了这种安全性之外,强力打字能带给你什么?
除安全性之外,强类型系统还有其他好处吗?
(请注意,我并不是说强打字是没有价值的。安全本身就是一个巨大的好处。我只是想知道是否还有额外的好处。)
答案 0 :(得分:25)
首先,我们需要谈谈简单输入的lambda演算的历史。
简单类型的lambda演算有两个历史发展。
当Alonzo Church描述lambda演算时,这些类型作为术语的含义/操作行为的一部分被烘焙。
当Haskell Curry描述lambda演算时,类型是注释的注释。
所以我们有lambda演算教堂和lambda演算la Curry。有关详情,请参阅https://en.wikipedia.org/wiki/Simply_typed_lambda_calculus#Intrinsic_vs._extrinsic_interpretations。
具有讽刺意味的是,以库里命名的Haskell语言是基于lambda演算的教堂!
这意味着类型不仅仅是为您排除不良程序的注释。他们也可以“做事”。这些类型不会在不留下残留物的情况下擦除。
这表现在Haskell的类型类概念中,这就是为什么Haskell是一种教会语言的原因。
在Haskell中,当我创建一个函数时
sort :: Ord a => [a] -> [a]
我们将Ord a
的对象或字典作为第一个参数传递。
但是你并没有被迫在代码中探讨自己的论点,编译器的工作就是构建它并使用它。
instance Ord Char
instance Ord Int
instance Ord a => Ord [a]
因此,如果你去对字符串列表进行排序,这些字符串本身是字符列表,那么这将通过将Ord Char实例传递给Ord a =>的实例来构建字典。 Ord [a]获取Ord [Char],它与Ord String相同,然后你可以对字符串列表进行排序。
调用上面的sort
比通过将LexicographicComparator<List<Char>>
传递给它的构造函数并使用额外的第二个参数调用函数来手动构建IComparator<Char>
要简单得多,如果我是比较在Haskell中调用这样的sort
函数到在C#或Java中调用它的复杂性。
这向我们表明,使用类型进行编程可能会明显减少冗长,因为类型检查期间,implicits和类型类等机制可以为程序推断出大部分代码。
在一个更简单的基础上,即使参数的大小也可以取决于类型,除非你想支付相当大的成本来用你的语言装箱所有内容,以便它具有同质的表示。
这向我们展示了使用类型编程可以显着提高效率,因为它可以使用专用表示,而不是在代码中的任何地方支付盒装结构。 int
不能只是一个机器整数,因为它必须看起来像系统中的其他所有东西。如果您愿意在运行时放弃一个数量级或更高性能,那么这对您来说无关紧要。
最后,一旦我们为我们提供“做事”的类型,考虑仅仅安全提供的重构效益通常是有益的。
如果我重构剩下的较小代码集,它将为我重写所有类型类管道。它将找出它可以重写代码以取消更多参数的新方法。我不会手工制作所有这些东西,我可以将这些平凡的任务留给类型检查器。
但是即使我确实改变了类型,我也可以毫不犹豫地移动参数,因为编译器很可能会发现我的错误。类型为您提供“自由定理”,就像整类此类错误的单元测试一样。
另一方面,一旦我用Python这样的语言锁定API,我就会害怕改变它,因为它会在运行时默默地为所有下游依赖项而破坏!这导致了巴洛克式的API,它们很容易依赖于容易腐烂的关键字参数,并且随着时间的推移,随着时间的推移而发展的API的内容很少类似于你开箱即用的内容。因此,即使仅仅是安全问题,一旦您希望人们在您的工作之上进行构建,而不是简单地在它过于笨拙时更换它,就会对API设计产生长期影响。
答案 1 :(得分:21)
这是因为打字只会删除程序,而不会启用新程序。
这不是一个正确的陈述。类型类使得从类型级信息生成程序的一部分成为可能。
考虑两个表达式:
readMaybe "15" :: Maybe Integer
readMaybe "15" :: Maybe Bool
我在这里使用readMaybe
模块中的Text.Read
函数。在术语级别,这些表达式是相同的,只有它们的类型注释是不同的。但是,它们在运行时生成的结果不同(第一种情况为Just 15
,第二种情况为Nothing
。
这是因为编译器会根据您的静态类型信息为您生成代码。更确切地说,它选择一个合适的类型类实例并将其字典传递给多态函数(在我们的例子中为readMaybe
)。
这个例子很简单,但有更复杂的用例。使用mtl
库,您可以编写在不同计算上下文中运行的计算(又名Monad
s)。编译器将自动插入大量管理计算上下文的代码。在动态类型语言中,您无法获得静态信息。
正如您所看到的,静态类型不仅会切断不正确的程序,还会为您编写正确的程序。
答案 2 :(得分:3)
重构。通过使用强类型系统,您可以安全地重构代码,让编译器告诉您现在正在做的事情是否有意义。打字系统越强,避免的重构错误就越多。这当然意味着您的代码更易于维护。
答案 3 :(得分:3)
当您已经知道自己想要写什么以及如何写作时,您需要“安全”。这是什么类型有用的很小一部分。关于类型最重要的是它们使你的推理结构化。当有人用Python addr
写作时,他没有看到a + b
和a
作为一些抽象变量 - 他将它们视为一些数字。类型已经存在于人类的内部语言中,Python只是没有类型系统来讨论它们。 “类型化与无类型(统一)编程”争议中的实际问题是“我们是否希望以安全,明确或不安全和隐含的方式反映我们的内部结构化概念?”。类型不会引入新概念 - 它是无类型推理会忘记现有概念。
当有人看到一棵树(我的意思是一个真正的绿色树)时,他并没有看到它上面的每一片叶子,但他并没有将它视为一个抽象的无名对象。 “树” - 对于大多数情况来说是一个足够好的近似值,这就是为什么我们有Hindley-Milner类型的系统,但有时你想谈论一个特定的树,你确实想要看叶子。这就是依赖类型给你的东西:缩放的能力。 “没有叶子的树”,“森林中的一棵树”,“特定形式的树”......依赖类型编程只是人类思考方式的又一步。
在一个不太抽象的说明中,我有一个类型检查器,用于依赖玩具类型的语言,其中所有键入规则都表示为data type的构造函数。您无需深入了解类型检查过程即可了解系统规则。这就是“缩放”的力量:你可以根据需要引入复杂的不变量,从而区分必要部分和不重要部分。
功率相关类型的另一个例子是给你各种形式的反射。看,例如在Pierre-ÉvaristeDagandthesis,证明了
泛型编程只是编程
当然,类型是提示,我定义的许多函数和抽象我会用弱类型语言以更笨拙的方式定义,但类型提示更好的替代方案。
毫无疑问“选择什么:简单类型或依赖类型?”。依赖类型总是更好,它们当然包含简单类型。问题是“选择什么:没有类型或依赖类型?”,但这个问题不适合我。