类型推断的权衡是什么?


29

似乎所有新的编程语言,或者至少是流行的编程语言都使用类型推断。即使是Javascript,也可以通过各种实现(Acscript,Typescript等)获得类型和类型推断。对我来说看起来很棒,但我想知道是否需要权衡取舍,或者为什么要说Java或旧的好语言没有类型推断

  • Go中声明变量而不指定其类型时(使用不带类型的var或:=语法)时,将从右侧的值推断出变量的类型。
  • D允许编写大型代码片段,而无需像动态语言那样重复指定类型。另一方面,静态推断可推导类型和其他代码属性,从而充分利用静态和动态环境。
  • Rust中的类型推断引擎非常聪明。它所做的不只是在初始化过程中查看r值的类型。它还看起来如何在以后使用变量来推断其类型。
  • Swift使用类型推断来得出适当的类型。通过类型推断,编译器只需检查您提供的值,即可在编译代码时自动推断出特定表达式的类型。

3
在C#中,一般准则说不要总是使用,var因为它有时会损害可读性。
Mephy 2015年

5
“ ...或者为什么要说Java或旧的好语言没有类型推断”的原因可能是历史原因;根据维基百科,ML出现在C之后1年,并且具有类型推断功能。Java 试图吸引C ++开发人员。C ++最初是对C的扩展,而C的主要关注点是在程序集上的可移植包装以及易于编译。无论值多少钱,我都读过,在一般情况下,子类型化也使类型推断变得不可确定。
2015年

3
对于支持子类型继承的语言,@ Doval Scala似乎在推断类型方面做得很好。它不如任何ML系列语言都好,但考虑到语言的设计,它可能会满足您的要求。
KChaloux

12
值得在类型推导(如C#var和C ++的单向auto)和类型推断(如Haskell的双向)之间进行区分let。在前一种情况下,名称的类型只能从其初始化程序中推断出-名称的使用必须遵循名称的类型。在后一种情况下,名称的类型也可以从其用法中推断出来–这很有用,因为您可以简单地[]为一个空序列编写代码,而不必考虑元素类型,也newEmptyMVar可以为新的null可变引用编写,而与引用对象无关类型。
乔恩·普迪

类型推断的实现起来非常复杂,特别是高效。人们不希望其编译复杂性因更复杂的表达式而遭受指数级爆炸。有趣的读物:cocoawithlove.com/blog/2016/07/12/type-checker-issues.html
亚历山大–恢复莫妮卡

Answers:


43

Haskell的类型系统是完全可推断的(抛开了多态递归,某些语言 扩展以及可怕的单态性限制),但是程序员即使在不需要时仍经常在源代码中提供类型注释。为什么?

  1. 类型注释用作文档。对于像Haskell那样具有表现力的类型尤其如此。给定一个函数的名称和类型,您通常可以很好地猜测该函数的功能,尤其是当该函数具有参数多态性时。
  2. 类型注释可以推动开发。在编写函数主体之前编写类型签名的感觉有点像测试驱动的开发。以我的经验,一旦编译了Haskell函数,它通常会在第一次运行。(当然,这并不能消除自动测试的必要!)
  3. 显式类型可以帮助您检查假设。当我试图理解一些已经可以使用的代码时,我经常在代码上加上我认为是正确的类型注释。如果代码仍然可以编译,我知道我已经理解了。如果没有,我将读取错误消息。
  4. 类型签名使您可以专门处理多态函数。在某些情况下,如果某些函数不是多态的,则API更具表达性或有用性。如果为函数提供的类型比推断的类型少,则编译器不会抱怨。典型的例子是map :: (a -> b) -> [a] -> [b]。它的一般形式(fmap :: Functor f => (a -> b) -> f a -> f b)适用于所有Functors,而不仅限于列表。但是,map对于初学者来说,这会更容易理解,因此它与它的大哥哥同住。

总体而言,静态类型化但无法推断的系统的缺点与静态类型化的缺点大体相同,在本网站以及其他网站上进行的讨论不尽如人意(在Google上搜索“静态类型化缺点”页的火焰战争)。当然,通过可推断的系统中较少数量的类型注释可以减轻某些上述缺点。另外,类型推断有其自身的优势:没有类型推断就不可能进行孔驱动开发

Java *证明,需要太多类型注释的语言会令人讨厌,但是如果使用的语言太少,您就会失去我上面描述的优点。选择退出类型推断的语言在​​这两种极端之间取得了令人满意的平衡。

*即使是Java,那个伟大的替罪羊,也会执行一定数量的本地类型推断。在类似的语句中Map<String, Integer> = new HashMap<>();,您不必指定构造函数的泛型类型。另一方面,ML风格的语言通常是全球可推断的。


2
但是您不是一个初学者;)您必须对类型类和类型有一个真正的“了解”之前的理解Functor。列表,map更容易为未经验的Haskellers所熟悉。
本杰明·霍奇森

1
您是否认为C#var是类型推断的示例?
本杰明·霍奇森

1
是的,C#var是一个恰当的例子。
2015年

1
该站点已经将这一讨论标记为冗长,因此这将是我的最后答复。在前者中,编译器必须确定类型x; 在后者中,没有类型可以确定,所有类型都是已知的,您只需要检查表达式是否有意义即可。当您跳过一些琐碎的示例时,区别变得更加重要,并x在多个地方使用;然后,编译器必须交叉检查x用于确定位置的位置1)是否可以分配x类型,以便代码进行类型检查?2)如果是,我们可以分配的最通用类型是什么?
2015年

1
请注意,new HashMap<>();仅在Java 7中添加了语法,而Java 8中的lambda允许进行很多“真实”类型推断。
Michael Borgwardt 2015年

8

在C#中,类型推断发生在编译时,因此运行时成本为零。

就样式而言,var用于手动指定类型不方便或不必要的情况。Linq就是这样一种情况。另一个是:

var s = new SomeReallyLongTypeNameWith<Several, Type, Parameters>(andFormal, parameters);

没有它们,您将重复很长的类型名称(和类型参数),而不是简单地说var

显式使用类型的实际名称可以提高代码的清晰度。

在某些情况下无法使用类型推断,例如在构造时设置其值的成员变量声明,或者您确实希望intellisense正常工作(除非明确声明类型,否则Hackerrank的IDE不会对变量的成员进行intellisense)。 。


16
“在C#中,类型推断发生在编译时,因此运行时成本为零。” 根据定义,类型推断发生在编译时,因此每种语言都是如此。
2015年

好多了。
罗伯特·哈维

1
@Doval可以在JIT编译期间执行类型推断,这显然会增加运行时成本。
2015年

6
@Jules如果您已经运行了程序,那么已经对它进行了类型检查。没有什么可以推断的。尽管您不确定适当的用语,但您通常所说的不是类型推断。
2015年

7

好问题!

  1. 由于未显式注释类型,因此有时可能会使代码更难阅读-导致更多错误。正确使用它当然会使代码更整洁,易读。如果您是一个悲观主义者,并且认为大多数程序员是坏人(或者在大多数程序员坏人的地方工作),那将是一笔净损失。
  2. 尽管类型推断算法相对简单,但并不是免费的。这种事情会稍微增加编译时间。
  3. 由于未显式注释类型,因此您的IDE无法很好地猜测您要做什么,从而在声明过程中损害了自动完成功能和类似的帮助程序。
  4. 结合函数重载,您有时会遇到类型推断算法无法决定采用哪条路径的情况,从而导致较难看的转换样式注释。(例如,这在C#的匿名函数语法中经常发生)。

还有更多深奥的语言,如果没有显式类型注释,就无法做到怪异。到目前为止,除了顺便说一句,我所知道的没有什么比这更普遍/受欢迎/有希望的。


1
自动完成功能不会给类型推断带来麻烦。决定类型的方式无关紧要,只有类型决定了。C#的lambda的问题与推理无关,这是因为lambda是多态的,但类型系统无法应对-与推理无关。
DeadMG

2
@deadmg-当然,一旦声明了变量,就没有问题,但是当您在变量声明中键入内容时,由于没有声明的类型,它无法将右侧的选项缩小为声明的类型。
Telastyn'2

@deadmg-至于lambda,我对您的意思不是很清楚,但是基于我对事物工作原理的理解,我很确定这不是完全正确的。甚至简单的事情也var foo = x => x;失败了,因为该语言需要在x这里进行推断并且没有其他内容。生成lambda时,将它们作为显式类型的委托Func<int, int> foo = x => x构建到CIL中,就像Func<int, int> foo = new Func<int, int>(x=>x);将lambda作为生成的显式类型的函数构建一样。对我来说,推断类型x是推断的一部分……
Telastyn

3
@Telastyn语言没有什么可继续的。输入表达式所需的所有信息x => x都包含在lambda本身内-它不引用周围范围的任何变量。任何理智的函数式语言会正确地推断出它的类型是“为所有类型的aa -> a”。我认为DeadMG想要说的是C#的类型系统缺少主体类型属性,这意味着总是有可能找出任何表达式的最通用类型。这是一个非常容易破坏的财产。
2015年

@Doval-嗯... C#中没有通用函数对象之类的东西,我认为这与你们都在谈论的东西有细微的差别,即使这会导致相同的麻烦。
Telastyn'2

4

对我来说看起来很棒,但我想知道是否需要权衡取舍,或者为什么要说Java或旧的好语言没有类型推断

Java碰巧是例外,而不是这里的规则。auto自C ++ 11标准以来,甚至C ++(我相信它也被认为是一种“很好的旧语言” :)都支持使用关键字进行类型推断。它不仅适用于变量声明,还可用作函数返回类型,这对于某些复杂的模板函数特别方便。

隐式类型和类型推断有很多很好的用例,在某些用例中,您真的不应该这样做。有时这是口味问题,也是辩论的主题。

但是毫无疑问,有很好的用例,这本身就是任何语言实现它的正当理由。这也不是很难实现的功能,不会造成运行时间损失,并且不会显着影响编译时间。

在给开发人员使用类型推断的机会方面,我没有看到真正的缺点。

一些回答者认为有时显式键入有多好,他们肯定是正确的。但是不支持隐式键入将意味着一种语言始终会强制执行显式键入。

因此,真正的缺点是当一种语言支持隐式类型时,因为它指出,开发人员没有正当理由使用它,这是不正确的。


1

Hindley-Milner类型推断系统和Go风格类型推断之间的主要区别是信息流的方向。在HM中,类型信息通过统一向前和向后流动。在Go中,类型信息仅向前流动:它仅计算向前替代。

HM类型推断是一个出色的创新,可以与多态类型的功能语言很好地配合使用,但是Go的作者可能会争辩说它试图做太多事情:

  1. 信息向前和向后流动的事实意味着HM类型推论是非常不本地的。在没有类型注释的情况下,程序中的每一行代码都可能导致单行代码的键入。如果只有正向替换,则知道类型错误的来源必须在错误之前的代码中;否则,错误代码将由错误代码替换。而不是后面的代码。

  2. 通过HM类型推断,您倾向于考虑约束:当您使用一种类型时,您要限制它可能具有的类型。在一天结束时,可能会有一些类型变量完全不受约束。HM类型推断的见解是,这些类型确实无关紧要,因此将它们变成多态变量。然而,由于多种原因,这种额外的多态性可能是不希望的。首先,正如某些人指出的那样,这种额外的多态性可能是不可取的:HM得出伪造的,某些伪造代码的多态类型,并随后导致奇怪的错误。其次,当类型保留为多态时,这可能会对运行时行为产生影响。例如,过度多态的中间结果就是“显示”的原因。在Haskell中,“ read”被认为是模棱两可的;再举一个例子


2
不幸的是,这看起来像是对另一个问题的很好的答案。OP询问的是“ Go样式类型推断”与根本没有任何类型推断的语言,而不是“ Go样式”与HM。
Ixrec

-2

它会损害可读性。

比较

var stuff = someComplexFunction()

List<BusinessObject> stuff = someComplexFunction()

当像github上的IDE之外阅读时,这尤其是一个问题。


4
在先前的6个答案中,这似乎并没有提供实质性的要点和解释
gnat

如果使用专有名称而不是“复杂的不可读”,则var可以使用它而不会损害可读性。var objects = GetBusinessObjects();
法比奥

好的,但是随后您将类型系统泄漏到了变量名中。这就像匈牙利的记法。重构之后,您更有可能最终得到与代码不匹配的名称。通常,功能样式会使您失去类提供的自然命名空间优势。因此,您最终会有更多squareArea()而不是square.area()。
miguel
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.