本文内容主要来自《Scala in Depth》的第一章。主要介绍Scala是一门集面向对象编程和函数编程的复合语言。
多范型语言
Scala除了是面向对象和函数式的语言外,它还是:
泛型语言
脚本语言
命令式语言
元语言
作为一门现代的编程语言,不可避免的要接受各种思考方式的影响。跟C++一样,语言本身提供的泛型支持很多,不一定适合各自的项目,需要依据自身情况选择合适的泛型。并且需要将这种选择规范化。并且Scala的书写方法更加灵活,也可以说是更加难以理解,对Scala的代码规范会更加重要。
Scala起源的主要理论支撑室函数式编程和Petri网络[注1]。
Scala的核心概念
由于面向对象和函数本身是不一样的分析问题的方法。对于一门糅合了这两种方法的语言,我们需要去尝试理解如何将它们有机地结合起来。对于Scala复合语言,要理解语言整合三个重要方面:
面向对象和函数式的融合。面向对象的工作方式是定义和组合对象;函数式的工作方式是定义和组合函数。在Scala里面,函数也是对象。用Scala写程序,需要的不仅仅是定义和组合对象,还需要定义和组合函数。
富有表现力的语言和静态类型。主流的静态类型语言,比如Java、C++要求明确的类型。这造成代码中夹杂了太多了类型繁复信息。一个最佳实例就是老版本C++中STL的一些代码。Scala的代码可以像动态类型语言,比如Ruby,非常简洁,但同时还是类型安全的。
高级语言特性和深度的Java整合。Scala自身有很多高级的语法特性,比如柯里化、延迟求值等。但另一方面又要求整合Java,使Scala和Java能够互操作。
上面的3个方面既是我们想要的,但它们自身又是是矛盾的。Scala找到了它自己的整合方式。
动词VS名词
面对对象的核心是实体,是找到活动中的名词成分,然后定义它们的交互。函数式的核心是找到活动中的动词成分,然后将动作组合成一个连续的动作组合。我们举个例子:
猫抓住并吃掉了鸟。
一个面向对象的程序员看到这句话,就会知道两个实体:猫和鸟,还有两个动作:抓和吃。写出来的程序:
class Bird class Cat { def catchBird(b: Bird): Unit = Console.println("Catch Bird") def eat(): Unit = Console.println("Eat Bird") } object CatBird extends scala.App { val cat = new Cat val bird = new Bird cat.catchBird(bird) cat.eat() }
一个喜欢函数式的程序员看到这句话,想到的就不一样。他首先看到两个动作:抓和吃。对于抓这个动作,需要一个掠食者一个牺牲者,返回这个掠食者;对于吃这个动作,需要一个掠食者,吃了就该不饿了,饱了。因此他写出来的程序可能像:
trait Cat trait Bird trait Catch trait FullTummy class BlackCat extends Cat with Catch with FullTummy class WhithBird extends Bird object CatBird extends scala.App { def catchBird(hunter: BlackCat, prey: WhithBird): BlackCat = { Console.println("Catch Bird") hunter } def eat(consumer: BlackCat): BlackCat = { Console.println("Eat Bird") consumer } def story(hunter: BlackCat, prey: WhithBird) : Unit = { eat(catchBird(hunter, prey)) } story(new BlackCat, new WhithBird) }
由于使用Scala描述,上面的代码还交杂着面向对象的东西。由于函数还是需要有输入,输入就是我们的实体。因此函数式编程比面向对象,有更强的类型依赖。
我们来总结一下面向对象和函数式的软件开发方式:面向对象是一种自顶向下的代码设计方法。它先把软件分解为不同的名词或者对象,然后每个对象有ID(self/this)、方法,还有状态。在识别出所有的名词和它们的行为之后,名词之间的交互也就定义完成了。函数式将软件分解为不同的行为、动作,是一种自底向上的设计方法。函数式夹杂有很强的数学思维,行为和动作只是作用于输入,没有副作用。所有的变量都视为是不可变的。
Object-oriented programming | Functional programming |
Composition of objects (nouns) | Composition of functions (verbs) |
Encapsulated stateful interaction | Deferred side effects |
Iterative algorithms | Recursive algorithms and continuations |
Imperative flow | Lazy evaluation |
N/A | Pattern matching |
Scala之外的函数式思维
由于函数式编程的很多好处。在Scala之外,我们常用的Java中都可以看到函数式思维的成功运用。举个例子,EJB。EJB中有Session Bean,它用来包含行为。Entity Bean,用来对系统中的实体进行建模。Stateless Session Bean有点类似于函数式中的集合。
最近的Java 8也新增加有函数接口。
Function和Predicate接口。
Scala为语言的表达能力所做的设计决策
主要的设计决策包括:
类型在右边,修饰符在左边。
类型推断
Scalable syntax
隐式转化
对于类型放在左边还是右边,估计有不少争议。主要还是来自于继承古老C,而养成的习惯问题。我们做个比较,不下结论。
Variable type | C++ | Java | Scala |
Mutable integer variable | int x | int x | var x: Int |
Immutable integer value | const int x | final int x | val x: Int |
Constant pointer to a volatile integer | volatile int * const x | N/A | N/A |
Lazily evaluated integer value | N/A | N/A | lazy val x: Int |
类型推断是Scala能够保持简洁,又是类型安全语言的根本原因。从本质上讲,静态类型并不需要显式地为编译器指定所有标示符的类型,原因在于编译器自身的技术原因。而现在,编译器能够做类型推断了。最新的C++也支持auto,类型推断肯定是主流的语言发展方向。
第三项,可变语法对软件工程来讲可能不是好消息。因为它增加了理解、交流的难度。比如:
def qsort1[T <% Ordered[T]](list: List[T]): List[T] = { list match { case Nil => Nil; case x :: xs => val (before, after) = xs.partition({ i => i.<(x) }) qsort1(before).++(qsort1(after).::(x)) } } def qsort2[T <% Ordered[T]](list: List[T]): List[T] = list match { case Nil => Nil case x :: xs => val (before, after) = xs partition (_ < x) qsort2(before) ++ (x :: qsort2(after)) }
哪个看着好点呢?
隐式类型转换,如果使用过C++就该很熟悉了。原生类型之间只要不溢出可以相互转换,并且不需要显式指明。Scala在两种情况下会尝试隐式转换:一是,类型不匹配的时候;二是,改类型不存在某个方法的时候。
Scala和Java的互操作性
互操作涉及两个方面,一是Scala访问Java;二是Java访问Scala。我们只是看前者,后者使用的比较少,支持也不完整,也没有多少意义。
Scala的类继承结构图:
Scala的AnyRef等价于Java的Object。
需要注意的是Nothing和Null,它们是类型的下限。这在其他面向对象的语言里面是没有的。
在Scala里面使用Java是非常顺畅的。因为存在Scala到Java的直接映射。Java的class在Scala里面还是class。Java的interface在Scala里面是trait。Java的静态成员在Scala里面是object。Scala的包包含的使用方法,不管包含Scala包还是Java包,都可以像Java那样使用。
注1:Petri网是对离散并行系统的数学表示。Petri网是1960年代由卡尔·A·佩特里发明的,适合于描述异步的、并发的计算机系统模型。它的提出主要是解决形式既有语言缺少并行支持的问题。