印度朋友手把手教你学Scala(2):Scala里的类型推断 & 类型


/**
 * 谨献给我最爱的YoYo
 * 原文出处:https://madusudanan.com/blog/scala-tutorials-part-2-type-inference-in-scala/
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-02-25
 */

本翻译已征得Madusudanan.B.N同意,并链接在原文教程前面。

类型推断

这是关于Scala系列教程的第二章。点击这里可查看整个系列。

如果你还记得的话,在上一章,我们通过一些示例简短地讲解了推断推断。现在我们准备通过更多的示例继续进行深挖,以下是这篇文章的主要内容。

温馨提示:这是一个枯燥无味的主题,很多都是基本的理论,但也很重要。我已经努力尽可能以最简约覆盖本质的内容。此外,提供了很多材料以供业余学习。

目录

  • 从程序员的视角,到底什么是类型推断
  • 类型系统概览
  • 根据类型系统的语言分类
  • 类型推导器(HM)类型推断
  • 本地 vs 全局类型推断 -- 为什么Scala选择本地推断推断
  • Scala类型系统和子类型的简单介绍
  • 何时使用类型推断
  • 参考

从程序员的视角,到底什么是接口类型

对于Scala来说,这没什么特别。很多语言,例如OCaml,Haskell,Rust,Swift,C#(3.0版本开始) 都已经有这些了。

先从维基百科的定义开始。

类型推论、类型推断、或隐含类型,是指编程语言在编译期中能够自动推导出值的数据类型的能力,它是一些强静态类型语言的特性。

好吧,就像我们在上一篇文章看到的那样,很明显它会自动推断类型。

早期的目的是为了帮助程序员避免啰嗦的输入但仍然可以维护静态类型语言编译时的类型安全。

简单来说,类型推断是静态类型和动态类型这两个世界里最好的。

或者至少它尽力成为这样。实际上很大程度上取决于我们在哪里/如何使用它。

类型系统概览

类型系统是一个负责类型检查的语言容器。Scala是静态类型语言,所以通常会有一系列类型,而任何不在那个系列里的东西都会被视为非法类型,并且会抛出一个编译时错误。

为什么一点也不容纳任何其他类型系统呢?

因为计算机匹配不了人类的失误,而既定的东西更容易被计算机处理,而不是依靠人来把它给调整正确。它也可以防止由于合适类型而冒出的一大堆Bug。类型系统存在是为了提供类型安全,而不同的语言和不同的运行时,有不同的严格级别。

这把我们带到了另外一个问题,存在哪些类型系统,以及根据哪一个我们可以区别不同的语言。

根据类型系统的语言分类

引自这篇维基百科的页面

  • 动态检查
  • 静态检查
  • 显示宣告和隐式暗示(Inferred vs Manifest)
  • 标明类型系统和结构类型系统(Nominal vs Structural)
  • 依存类型(Dependant typing)
  • Gradual typing
  • Latent typing
  • 子类型
  • Uniqueness typing
  • 强类型和弱类型

译者注:更多从维基百科页面引用过来的类型如下。

  • 复合类型
  • 派生类型
  • 对象类型
  • 不完全类型
  • 递归类型
  • 函数类型
  • 全称量化类型
  • 存在量化类型
  • 精炼类型
  • 所有权类型

这么多系统,看得都让人头晕。我也非常推荐你听下这门课程,以便了解理多。视频可以下载/离线观看。

这可能也会出现在[course]https://www.coursera.org/course/proglang)上,所以确保你把它添加到了你的待看列表里。

这里还有另一个相当好的课程

综上所述,Scala可以分类成一门带类型推断的静态类型语言。函数式编程和类型推断之间有很强的关系,后面我们还会一次又一次讲到。

类型推导器(HM)类型推断

关于类型推断,我们可以谈论好几天,但最著名的算法应该当数HM类型推断算法

HM算法检查程序从而推断出它属于什么类型。如果你已经听完了上面的课程,那么对于这意味着什么就会有一个相当明朗的概念。

以下是一个典型带类型推断的类型系统是如何工作的例子。它会构建一棵包含全部元素的解析树,分析全部元素可能属于的类型,并且达到一个最终结论。

以上示例是伪代码,语法并不重要。如果总和小于10则返回true,否则大于等于10则返回false。我们可以从这个示例翻译/构建成另一个复杂的工作流。

很多算法几乎都是以相同的方式工作。如果有类似把两个字符串相乘的任何错误,它会抛出一个异常。

一些入门级的Haskell编程真的会有助于更好地理解这一点。Learn you a haskell是一个很好的入门网站。

Hindley-Milner算法也被称为全局类型推断。它读取整段代码并且推断类型。Scala类型系统的工作方式和上面解释的有一点不同。

本地 vs 全局类型推断 -- 为什么Scala选择本地类型推断

Scala遵循子类型和本地类型推断的结合。我打算把它和Haskell比较,因为它是最出名的函数式编程(FP)范式语言之一。

让我们通过另外一个示例来加深理解。

考虑以下代码,在Intellij里会产生一个编译时错误。

def factorial(a: Int) = {  
    if (a <= 1) 1 else a * factorial(a - 1)
}

错误信息如下:

现在可先忽略语法细节(当学习方法时我们会更深入进行讲解)。这段程序会计算传入数字的阶乘。如果我们看到了这些错误,说明预编译器不能推断出递归函数返回的类型。相同(类似)的Haskell代码则没有任何错误。

let factorial 0 = 1; factorial n = n * factorial (n - 1)  

当在Haskell GHCI(类似Scala的REPL)里执行上面代码,无编译错误。

这是现实世界一个全局和本地类型推断的例子。在Scala,本地类型推断帮助不了的地方我们都不得不标注类型(也请参考下面的:何时使用类型推断)。

上面代码的正确版本应许如下。注意在上面代码中没出现的Int类型在这里被明确指定了。

def factorial(a:Int): Int = {  
    if(a <=1) 1 else a * factorial(a-1)
}  

对于多范式语言,全局/类型推导真的真的很难,因为受限于例如继承和方法重载这些OOP特性。我们不打算过多地深究为什么像Haskell这样的语言做不到这些事情(如果你对系统编程/编译器黑客感兴趣,在网上有很多很多资源),但重点是Scala做了一个不同的交易。

Scala类型系统和子类型的简单介绍

如上所述,类型系统是由预定义的类型组件构成,并且这形成了scala如何继承的基础。

一图胜千言,你可以尝试使用一般IDE的“ctrl + 单击”深挖源代码,最后它都会指向Any类。请注意,类型不是正规的类,虽然他们看起来是。我们会在后面的文章中详细讨论。

Hindley-Milner算法不支持子类型,但本质上子类型存在于多范式世界里。这也是为什么Scala不使用HM算法的另一个原因。

让我们来通过下面的示例来理解子类型。

我们构建了一个异质列表。如果可能,子类型把低类型转换成高类型。第一个示例是把一个Int类型转换成Double类型的简单例子。如果不能适配,它会去到顶级,例如Any类型。全部这些转换都可以翻译成上面类型系统体系。

这使得面向对象编程处理起来非常简单。

更多信息,请访问Scala官方文档

何时使用类型推断

这里有一条细线通过类型推断把动态类型(无类型)和静态类型进行了划分。正如他们所说的那样“所有的代码都应该看起来像是优美的散文”,知道何时使用他们以及何时不使用很重要。

(1)何时使用?

它何时节省了编程时间和类型信息在哪并不重要。场景可以是在一个函数的内部,或者在一个循环里,此处关于类型的信息显而易见。

(2)何时不用?

简单地,例如它不应该让阅读代码的程序来猜测类型,这时类型信息是重要的。

它很难给出一个代码示例,因为它真的依赖于深思熟虑的应用。处理这种情况唯一极其简单的方式是和资深程序员一起评估代码走查,看下他们是否能理解他们。

随后,全部编写的代码为O(K),而阅读代码的为O(N),其中K会是一个常量,没有太多变动,因为只有一个人会写代码,而N会是你的团队中尝试阅读代码的人数。把K和团队大小相乘。

猜测来自错误,错误来自坏的代码,坏的代码来自挫败,失败导来自杀人的斧头

这是关于代码可读性,而不是其他。自由来自责任。

恭喜你!!如果目前你已经理解/到达了这里,那么你应该为自己感到骄傲。与其说这是一个相当难的主题,我会说它是一个围绕着你的大脑、不是很直观的主题。

敬请继续关注!!这只是个开始。

参考

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州