Scala教程:类型基础,scala教程类型
Scala教程:类型基础,scala教程类型
英文原文: Scala School,翻译:Wld5 - 朱伟杰
这一节的内容包含:
- 什么是静态类型?
- Scala中的类型
- 参数多态
- 参数推导:Hindley-Milener 与 局部类型推导
- Variance
- 范围(Bounds)
- 量化(Quantification)
什么是静态类型?为什么它们很有用?
根据Picrce的说法:“类型系统是一个可以根据代码段计算出来的值对它们进行分类,然后通过语法的手段来自动检测程序错误的系统。”
类型可以让你表示函数的域和值域。例如,在数学里,我们经常看到下面的函数:
f: R -> N
这个定义告诉我们函数”f”的作用是把实数集里的数映射到自然集里。
抽象地说,这才是具体意义上的类型。类型系统给了我们一些表示这些集合的更强大的方式。
有了这些类型标识,编译器现在可以 静态地(在编译期)判断这个程序是正确的。换句话说,如果一个值(在运行期)不能够满足程序里的限制条件的话,那么在编译期就会出错。
通常来说,类型检测器(typechecker)只能够保证不正确的的程序不能通过编译。但是,它不能够保证所有正确的程序都能通过编译。
由于类型系统的表达能力不断增加,使得我们能够生成出更加可靠的代码,因为它使得我们能够控制程序上的不可变,即是是程序还没有运行的情况下(在类型上限制bug的出现)。学术界一直在努力突破类型系统的表达能力的极限,包含值相关的类型。
注意,所有的类型信息都会在编译期擦除。后面不再需要。这个被称为类型擦除。
Scala中的类型
Scala强大的类型系统让我们可以使用更具有表现力的表达式。一些主要的特点如下:
- 支持参数多态,泛型编程
- 支持(局部)类型推导,这就是你为什么不需要写
val i: Int = 12: Int
- 支持存在向量(existential quantification),给一些没有名称的类型定义一些操作
- 支持视图。 我们下个星期再讨论;给定的值从一个类型到其他类型的“可转换性”
参数多态
多态可以用来编写泛型代码(用于处理不同类型的值),并且不会减少静态类型的表达能力。
例如,没有参数多态的话,一个泛型的列表数据结构通常会是下面这样的写法(在Java还没有泛型的时候,确实是这样的):
scala> 2 :: 1 :: "bar" :: "foo" :: Nil res5: List[Any] = List(2, 1, bar, foo)
这样的话,我们就不能够恢复每个元素的类型信息。
scala> res5.head res6: Any = 2
这样一来,我们的应用里会包含一系列的类型转换(“asInstanceOf[]“),代码会缺少类型安全(因为他们都是动态类型的)。
多态是通过指定类型变量来达到的。
scala> def drop1[A](l: List[A]) = l.tail drop1: [A](l: List[A])List[A] scala> drop1(List(1,2,3)) res1: List[Int] = List(2, 3)
多态是scala里的一等公民
简单来说,这意味着有一些你想在Scala里表达的类型概念会显得“太过于泛型”,从而导致编译器无法理解。假如你有这样一个函数:
def toList[A](a: A) = List(a)
你想要按照泛型的方式来使用它:
def foo[A, B](f: A => List[A], b: B) = f(b)
但是这样会编译不过,因为所有的类型变量在运行期必须是确定的。
def foo[A](f: A => List[A], i: Int) = f(i)
…you get a type mismatch.
…你会看到一个类型不匹配的错误
类型推导
对于静态类型的一个比较常见的缺陷就是有太多的类型语法。Scala提供了类型推导来解决这个问题。
函数式语言里比较经典的类型推导的方法是 Hindlry-Milner,并且它是在ML里首先使用的。
Scala的类型推导有一点点不同,不过思想上是一致的:推导所有的约束条件,然后统一到一个类型上。
在Scala里,例如,你不能这样写:
scala> { x => x } :7: error: missing parameter type { x => x }
但是在OCaml里,你可以:
# fun x -> x;; - : 'a -> 'a =
在Scala里,所有的类型推导都是局部的。Scala一次只考虑一个表达式。例如:
scala> def id[T](x: T) = x id: [T](x: T)T scala> val x = id(322) x: Int = 322 scala> val x = id("hey") x: java.lang.String = hey scala> val x = id(Array(1,2,3,4)) x: Array[Int] = Array(1, 2, 3, 4)
在这里,类型都被隐藏了。Scala编译器自动推导参数的类型。注意我们也没有必要显示指定返回值的类型了。
Varicace
Scala的类型系统需要把类的继承关系和多态结合起来。类的继承使得类之间存在父子的关系。当把面向对象和多态结合在一起时,一个核心的问题就出来了:如果T'是T的子类,那么Container[T']是不是Container[T]的子类呢?Variance注释允许你在类继承和多态类型之间表达下面的这些关系:
含义 | Scala中的标记 | |
covariant(协变) | C[T’]是C[T]的子类 | [+T] |
contravariant(逆变) | C[T]是C[T’]子类 | [-T] |
invariant(不变) | C[T]和C[T’]不相关 | [T] |
子类关系的真正意思是:对于一个给定的类型T,如果T’是它的子类,那么T’可以代替T吗?
scala> class Contravariant[-A] defined class Contravariant scala> val cv: Contravariant[String] = new Contravariant[AnyRef] cv: Contravariant[AnyRef] = Contravariant@49fa7ba scala> val fail: Contravariant[AnyRef] = new Contravariant[String] :6: error: type mismatch; found : Contravariant[String] required: Contravariant[AnyRef] val fail: Contravariant[AnyRef] = new Contravariant[String] ^
逆变(Contravariance)看起来比较奇怪。什么时候要用到它呢?确实让人感到惊讶!
trait Function1 [-T1, +R] extends AnyRef
如果你从替代的角度来看这个的话,非常容易理解这一点。我们首先来定义一个简单的类继承关系:
scala> class Animal { val sound = "rustle" } defined class Animal scala> class Bird extends Animal { override val sound = "call" } defined class Bird scala> class Chicken extends Bird { override val sound = "cluck" } defined class Chicken
假设你需要一个接收Bird
类型参数的函数:
scala> val getTweet: (Bird => String) = // TODO
标准的animal类库有一个函数可以完成你想要的任务,但是它的参数是Animal
。在大部分的场景下,如果你说“我需要一个,我有一个的子类”,这样是可以的。但是函数的参数都是可逆变的(contravariant),如果你需要一个接受一个Bird
的函数,并且你有一个接收一个Chicken
的函数,这个函数会卡在Duck
上。但是一个接收Animal
作为参数的函数就没有问题:
scala> val getTweet: (Bird => String) = ((a: Animal) => a.sound ) getTweet: Bird => String =
函数的返回值是可逆变的。如果你需要一个返回Bird
的函数,但是你只有一个返回Chicken
的函数,这样也是可以的。
scala> val hatch: (() => Bird) = (() => new Chicken ) hatch: () => Bird =
范围(Bounds)
Scala允许你通过 bounds 来限制多态的范围。这些范围表示的是子类的关系。
scala> def cacophony[T](things: Seq[T]) = things map (_.sound) :7: error: value sound is not a member of type parameter T def cacophony[T](things: Seq[T]) = things map (_.sound) ^ scala> def biophony[T <: Animal](things: Seq[T]) = things map (_.sound) biophony: [T <: Animal](things: Seq[T])Seq[java.lang.String] scala> biophony(Seq(new Chicken, new Bird)) res5: Seq[java.lang.String] = List(cluck, call)
还可以支持更低的类型范围;它们可以通过逆变(contravariance)和合适的协变(covariance)来实现。List[+T]
是协变量;一个Bird的列表同时也是一个Animal的列表。List
定义了一个操作符::[elem T]
返回一个装载elem
的列表。这个新的List
和原来的列表有着相同的类型:
scala> val flock = List(new Bird, new Bird) flock: List[Bird] = List(Bird@7e1ec70e, Bird@169ea8d2) scala> new Chicken :: flock res53: List[Bird] = List(Chicken@56fbda05, Bird@7e1ec70e, Bird@169ea8d2)
List
同时 也定义了::[B >: T]
,它返回一个List[B]
。注意B >: T
,它表示T
的父类。这个会在我们把一个Animal
添加到一个List[Bird]
的时候提醒我们进行纠正。
scala> new Animal :: flock res59: List[Animal] = List(Animal@11f8d3a8, Bird@7e1ec70e, Bird@169ea8d2)
注意返回的类型是Animal
。
量化(Quantification)
有时候你不需要给一个类型变量以名称,例如
scala> def count[A](l: List[A]) = l.size count: [A](List[A])Int
你可以用“通配符”来替代:
scala> def count(l: List[_]) = l.size count: (List[_])Int
这个可以替代:
scala> def count(l: List[T forSome { type T }]) = l.size count: (List[T forSome { type T }])Int
注意量化(quantification)可能会显得比较诡异:
scala> def drop1(l: List[_]) = l.tail drop1: (List[_])List[Any]
突然之间我们丢失了类型信息!为了一探究竟,我们来看看最原始的语法:
scala> def drop1(l: List[T forSome { type T }]) = l.tail drop1: (List[T forSome { type T }])List[T forSome { type T }]
我们不能说明T的任何信息,因为在这里的类型不允许。
你也可以对通配符类型使用范围来进行限定:
scala> def hashcodes(l: Seq[_ <: AnyRef]) = l map (_.hashCode) hashcodes: (Seq[_ <: AnyRef])Seq[Int] scala> hashcodes(Seq(1,2,3)) :7: error: type mismatch; found : Int(1) required: AnyRef Note: primitive types are not implicitly converted to AnyRef. You can safely force boxing by casting x.asInstanceOf[AnyRef]. hashcodes(Seq(1,2,3)) ^ scala> hashcodes(Seq("one", "two", "three")) res1: Seq[Int] = List(110182, 115276, 110339486)
参考 D. R. MacIver的Scala中的存在类型
英文原文: Scala School,翻译:Wld5 - 朱伟杰
译文链接:http://www.wld5.com/4126.html
【如需转载,请在正文中标注并保留原文链接、译文链接和译者等信息,谢谢合作!】
用户点评