转载

多面编程语言Scala

回首初次接触Scala,时光已忽忽过去四五年。从当初“Scala取代Java”的争论,到今天两者的相安无事, Scala带给了我们哪些有意义的尝试呢?在我掌握的 众多编程语言之中,Scala无疑是其中最让我感到舒 适的,如Scala官网宣称的:“Object-Oriented Meets Functional”,这一句当属对Scala最抽象的精准描述, 它把近二十年间大行其道的面向对象编程与旧而有之 的函数式编程有机结合起来,形成其独特的魔力。不 知你是否看过梁羽生的著作《绝塞传烽录》?里面白 驼山主宇文博的绝学:左手“寒冰掌”、右手“火焰 刀”,用来形容Scala最为合适了,能够将OOP与FP结 合得如此完美的语言,我认为唯有Scala。

众所周知,Java称不上纯粹的面向对象语言,但Scala 却拥有纯粹的面向对象特性,即便是1 + 1这么简单的 事情,实际上也是执行1.+(1)。而在对象组合方面, Scala拥有比接口更加强大的武器 ── 特质(trait)。

Scala同时作为一门函数式编程语言,理所当然地具备 了函数式语言的函数为头等“公民”、方法无副作用 等特性。事实上,Scala更吸引我的并不是OOP特性, 而是FP特性!一边是OOP、一边是FP,这就是多面的 Scala,极具魅力而且功能强大。

在多核时代,现代并发语言不断涌现出来,例如 Erlang、Go、Rust,Scala当然也位列其中。Scala的并 发特性,堪称Scala最吸引开发者的招牌式特性! Scala是静态类型的。许多人会把val s = "ABC"这样的 当作动态类型特性,而val s: String = "ABC"才认为是 静态类型特性。实际上,这无关类型争论,而是类型 系统实现的范畴。是的,在Scala里,你可以放心大胆 地使用val s = "ABC",而Scala里强大的类型推断和模 式匹配,绝对会让你爱不释手。

此外,Scala作为JVM语言,理所当然享有Java庞大 而优质的资源,与Java间可实现无缝交互,事实上, Scala最终当然是编译为Java字节码。

本文将把重点放在Scala的特色之处。作为一门完备而 日趋成熟的语言,Scala的知识点有不少,本文当然无 法做到面面俱到,但希望能够带你感受Scala魅力,并 理解其重要概念。

Scala的面向对象 

开胃菜 ── 类的定义 

来看个开胃菜,定义一个类:

多面编程语言Scala

我们知道,动态语言一般都提供了REPL环境,同时, 动态语言的程序代码都是以脚本方式解释运行的,这 给开发带来了不少的便利。Scala虽然是静态类型系统 的语言,但同样提供了这两个福利,让你倍感贴心。

因 此,你可以任意采取以下运行方式:   

  • 在命令行窗口或终端输入:scala,进入Scala的 REPL窗口,逐行运行上述代码;   
  • 此外,也可以将上述代码放入某个后缀名为.scala 的文件里,如test.scala,然后通过脚本运行方式运行: scala test.scala。 

测试信息“小强今年32岁,是一名程序员”结果出 来了! 

多么简单,类的定义就这么多,却能够做这么多事情, 想想Java的实现吧,差别太大了。我们先来分析下代 码。假设在上述第二种方式的test.scala文件中,注释 掉后面两行并保存,运行:

  • scalac test.scala 
  • javap -p Person

我们先是把文件编译成字节码(这实际上是跟Java编 译对应的第三种编译/运行方式),之后反编译并查看 结果:

多面编程语言Scala

这个结果跟Java实现的代码类似(生成的getter和 setter跟Java实现有所不同,但在这里不是什么问 题),可见,Scala帮我们做了多少简化工作。 这段代码有以下值得注意的地方:

我们可以把字段定义和构造函数直接写在Scala的 类定义里,其中,关键字val的含义是“不可变”,var 为“可变”,Scala的惯用法是优先考虑val,因为这更 贴近函数式编程风格;

  • 在Scala中,语句末尾的分号是可选的;   
  • Scala默认类访问修饰符为public;   
  • 注意println("测试信息")这一行,将在主构造函数 里执行;   
  • val与var两者对应Java声明的差异性已在反编译代 码中体现了。
伴生对象与伴生类

伴生对象与伴生类在Scala的面向对象编程方法中占据 极其重要的位置,例如Scala中许多工具方法都是由伴 生对象提供的。

伴生对象首先是一个单例对象,单例对象用关键字 object定义。在Scala中,单例对象分为两种,一种是 并未自动关联到特定类上的单例对象,称为独立对象 (Standalone Object);另一种是关联到一个类上的单 例对象,该单例对象与该类共有相同名字,则这种单 例对象称为伴生对象(Companion Object),对应类称 为伴生类。

Java中的类,可以既有静态成员,又有实例成员。而 在Scala中没有静态成员(静态字段和静态方法),因 为静态成员从严格意义而言是破坏面向对象纯洁性 的,因此,Scala借助伴生对象来完整支持类一级的属 性和操作。伴生类和伴生对象间可以相互访问对方的 private字段和方法。

接下来看一个伴生类和伴生对象的例子(Person. scala)。

多面编程语言Scala

这是一个典型的伴生类和伴生对象的例子,注意以下 说明:

  • 伴生类Person的构造函数定义为private,虽然这不 是必须的,却可以有效防止外部实例化Person类,使 得Person类只能供对应伴生对象使用;   
  • 每个类都可以有伴生对象,伴生类与伴生对象写在 同一个文件中;   
  • 在伴生类中,可以访问伴生对象的private字段 Person.uniqueSkill;   
  • 而在伴生对象中,也可以访问伴生类的private方法 Person.getUniqueSkill();   
  • 最后,在外部不用实例化,直接通过伴生对象访问 Person.printUniqueSkill()方法。
特质(Trait) 

Scala的特质类似于Java中的接口作用,专门用来解决 现实编程中的横切关注点矛盾,可以在类或实例中混 入(Mixin)这些特质。实际上,特质最终会被编译成 Java的接口及相应的实现类。 Scala的特质提供的特性远比Java的接口灵活,让我们 直接来看点有趣的东西吧。

多面编程语言Scala

多面编程语言Scala

我们先是定义了一个Programmer抽象类。最后定义了 四个不同程序员的Trait,且都继承自Programmer抽象 类,然后,通过不同的特质排列组合,看看我们产生的 结果是什么样子的:

所有程序员都至少掌握一门编程语言。

我掌握Scala。 我掌握Golang。 

所有程序员都至少掌握一门编程语言。

我掌握Scala。我掌握Golang。我掌握PHP。 ......

Wow~! 有趣的事情发生了,通过混入不同的特质组 合,不同的程序员都可以有合适的词来介绍自己,而 每个程序员的共性就是:“所有程序员都至少掌握一 门编程语言”。让我们来解释一下具体思路:   

这段代码里面,特质通过with混入实例,如:new  Programmer with Scalaist。当然,特质也可以混入 类中;   

  • 为什么信息可以传递呢?比如我掌握Scala。我掌握 Golang。我掌握PHP?答案就在super.getSkill()上。 该调用不是对父类的调用,而是对其左边混入的Trait 的调用,如果到左边第一个,就是调用Programmer抽 象类的getSkill()方法。这是Trait的一个链式延时绑 定特性,那么在现实中,这个特性就表现出极大的灵 活性,可以根据需要任意搭配,大大降低代码量。 

Scala的面向对象特性,暂先介绍到这里。其实还有好 些内容,限于篇幅,实在是有点意犹未尽的感觉。

Scala的函数式风格 

Scala的魅力之一就是其函数式编程风格实现。如果把 上面介绍的面向对象特性看成是Scala的“寒冰掌”, 让你感受到了迥异于Java实现的特性,那么,Scala 强大而魔幻的函数式特性,就是其另一大杀招“火焰 刀”,喷发的是无坚不摧的怒焰之火。

集合类型 

Scala常用集合类型有Array、Set、Map、Tuple和List等。 Scala提供了可变(mutable)与不可变(immutable)的 集合类型版本,多线程应用中应该使用不可变版本,这 很容易理解。   

  • Array:数组是可变的同类对象序列;   
  • Set:无序不重复集合类型,有可变和不可变实现;   
  • Map:键值对的映射,有可变和不可变实现;   
  • Tuple:可以包含不同类元素,不可变实现;   
  • List:Scala的列表是不可变实现的同类对象序列, 因应函数式编程特性的需要。
  • List大概是日常开发中使 用最多的集合类型了。 

这些集合类型包含了许多高阶函数,如:map、find、 filter、fold、reduce等等,构建出浓郁的函数式风格用 法,接下来我们就来简单了解一下:

多面编程语言Scala

输出如下: 

JavaScript很棒~ 

Scala很棒~ 

Golang很棒~ 

map() 函数在List上迭代,对List中的每个元素,都会 调用以参数形式传入的Lambda表达式(或者叫匿名函 数)。其结果是创建一个新的List,其元素内容都发生 了相应改变,可以从输出结果观察到。注意,代码中有 一行是速写法代码,我个人比较喜欢这种形式,但在 复杂代码中可读性差一些。 

最后,我们用了另一个foreach()方法来迭代输出 结果。 

高阶函数、Lambda表达式,都是纯正的函数式编程风 格。如果你接触过Haskell,就会发现Scala函数式风格 的实现,在骨子里像极了Haskell,感觉非常亲切。在 编写Scala代码的过程中,将处处体现出它的函数式编 程风格,高效而简洁。 

限于篇幅,我们只能浅尝辄止,如果有兴趣,可以进一 步参考我以前写的两篇相关博文,里面有比较详细的 描述: 七八个函数,两三门语言㈠和七八个函数,两三门语 言㈡•完结篇。

高阶函数、柯里化、不全函数和闭包

实际上我们在前面已经见识过Scala的高阶函数 (Higher-order Function)了,只不过是Scala自带的 map()和foreach()。高阶函数在维基百科中的定义 是:“高阶函数是至少满足下列一个条件的函数:接 受函数作为输入;输出一个函数”。 接下来,我们来实现一个自己的高阶函数──求圆周 长和圆面积:

多面编程语言Scala

我们定义了一个高阶函数cycle。输入参数中传入一个 函数值calc,其类型是函数,接收Float输入,输出也是 Float。在实现里,我们会调用calc函数。在调用时,我 们分别传入求圆周长和圆面积的匿名函数,用于实现 calc函数的逻辑。 

这样,我们用一个高阶函数cycle,就可以满足求圆周 长和圆面积的需求,不需要分别定义两个函数来处理 不同任务,而且代码直观简洁。最后,我们打印结果, 输出一组半径分别对应的圆周长和圆面积。在这里, 我们用到了映射Map:

圆周长:Map(1.0 -> 6.28, 2.3 -> 14.444, 4.5 -> 28.26) 

圆面积:Map(1.0 -> 3.14, 2.3 -> 16.6106, 4.5 -> 63.585) 

接下来,我们对上述代码稍加改动:

多面编程语言Scala

输出结果同上。 

注意到了吗?我们把cycle函数的两个输入参数进行了 拆分(如上述代码第一行),同时在调用cycle函数时, 方式也有所不同(如上述代码最后两行)。这是什么 意思? 

这在函数式编程中称为柯里化(Curry),柯里化可以 把函数定义中原有的一个参数列表转变为接收多个参 数列表。在函数式编程中,一个参数列表里含多个参 数的函数都是柯里函数,可以柯里化。 

要知道,在函数式编程里,函数是一等的,当然函数也 可以作为参数和返回被传递。这对初次接触函数式编 程的开发者而言确实比较抽象。上述代码的理解,你可以这样想象: (cacl: Float => Float)是函数cycle2(r: Array[Float]) 的输入参数!进一步,可以这么理解:cacl取一个参 数,变成了一个不全函数(Partially Function)cycle2 (r: Array[Float]),所谓不全函数就是它还有参数未确 定,你想要完整用它的话,还需要继续告知它未定的 参数,如(cacl: Float => Float)。 

还没完!根据上述描述,我们继续看看如何用各种 Hacker的调用方式:

多面编程语言Scala

可以用val c21 = cycle2 _、val c22 = cycle2(Array (1.0f, 2.3f, 4.5f)) _诸如此类的方式创建不全函数,并 调用它。 

看得出来,不全函数同样可以提升代码的简洁程度, 比如本例代码中,参数Array(1.0f, 2.3f, 4.5f)是固定不 变的,我们就不用每次都在调用cycle2时传入它,可以 先定义c22,再用c22来处理。 

函数式崇尚的“函数是第一等公民”理念可不容小 觑。函数,就是这么任性! 接下来,我们来了解下闭包(Closure)的概念,依旧先 看个简单的例子:

多面编程语言Scala

这个例子用来求圆柱体的体积。这里定义了一个 caclCylinderVolume函数(因为函数式风格里函数是一 等公民,所以可以用这样的函数字面量方式来定义。 或者也可以称之为代码块),函数里面引用了一个自 由变量high,caclCylinderVolume函数并未绑定high。 而在caclCylinderVolume函数运行时,要先“闭合” 函数及其所引用变量high的外部上下文,这样也就绑 定了变量high,此时绑定了变量high的函数对象称为 闭包。 

由代码可知,由于函数绑定到了变量high本身,因此, high如果发生改变,将影响函数的运算结果;而如果 在函数里更新了变量,那这种更新在函数之外也会被 体现。

模式匹配(Pattern Matching) 

Scala的模式匹配实现非常强大。模式匹配为编程过 程带来了莫大便利,在Scala并发编程中也得到了广泛 应用。

多面编程语言Scala

输出结果如下: 

多面者Scala~ 

你的Scala版本是:2.11.6 

八成是干净简洁的Go PHP语言呢? 

可见,模式匹配特性非常好用,可以灵活应对许多复 杂的应用场景:  

  • 第一个case表达式匹配普通的字面量;   
  • 第二个case表达式匹配正则表达式;   
  • 第三个case表达式使用了if判断,这种方式称为模 式护卫(Pattern Guard),可以对匹配条件加以过滤;   
  • 第四个case表达式使用了“_”来处理未匹配前面 几项的情况。 

此外,Scala的模式匹配还有更多用法,如case类匹 配、option类型匹配,同时还能带入变量,匹配各种 集合类型。综合运用模式匹配,能够极大提升开发效率。

并发编程 

现代语言的特性往往是随硬件环境和技术趋势演进 的,多核时代的来临,互联网大规模复杂业务处理,都 对传统语言提出了挑战,于是,新展现的语言几乎都 非常关注并发特性,Scala亦然。 

Scala语言并发设计采用Actor模型,借鉴了Erlang的 Actor实现,并且在Scala 2.10之后,改为使用Akka Actor模型库。Actor模型主要特征如下:  

  •  “一切皆是参与者”,且各个actor间是独立的;   
  • 发送者与已发送消息间解耦,这是Actor模型显著特 点,据此实现异步通信;   
  • actor是封装状态和行为的对象,通过消息交换进行 相互通信,交换的消息存放在接收方的邮箱中;   actor可以有父子关系,父actor可以监管子actor,子 actor唯一的监管者就是父actor;   
  • 一个actor就是一个容器,它包含了状态、行为、 一个邮箱(邮箱用来接受消息)、子actor和一个监管 策略。 

我们先来看个例子感受下:

多面编程语言Scala

多面编程语言Scala

在这里,Concurrency是CalcActor的父actor。在 Concurrency中先要构建一个Akka系统:

多面编程语言Scala

同时,这里的设置将会在线程池里初始化称为 “routee”的子actor(这里是CalcActor),数量为4,也 就是我们需要4个CalcActor实例参与并发计算。这一 步很关键。actor是一个容器,使用actorOf来创建Actor 实例时,也就意味着需指定具体Actor实例,即指定哪 个actor在执行任务,该actor必然要有“身份”标识, 否则怎么指定呢?! 

在Concurrency中通过以下代码向CalcActor发送序号 并启动并发计算:

for (i <- 1 to 4) calcActor ! i

然后,在CalcActor的receive中,通过模式匹配,对接 收值进行处理,直到接收值处理完成。在运行结果就 会发现每次输出的顺序都是不一样的,因为我们的程 序是并发计算。比如某次的运行结果如下。 

  • 序号为: 1。 
  • 序号为:3。 
  • 序号为:2。 
  • 序号为:4。 

actor是异步的,因为发送者与已发送消息间实现了解 耦;在整个运算过程中,我们很容易理解发送者与已 发送消息间的解耦特征,发送者和接收者各种关心自 己要处理的任务即可,比如状态和行为处理、发送的 时机与内容、接收消息的时机与内容等。当然,actor 确实是一个 容器 ,且 五脏俱全 :我们用类来封 装,里面也封装了必须的逻辑方法。Akka基于JVM, 虽然可以穿插混合应用函数式风格,但实现模式是 面向对象,天然讲究抽象与封装,其当然也能应用于 Java语言。 我们的Scala之旅就要告一个段落了!Scala功能丰富而 具有一定挑战度,上述三块内容,每一块都值得扩展 详述,但由于篇幅关系,在此无法一一展开。

希望通过 本文能够吸引你去了解、尝试Scala,体验一下其独特魅力,练就自己的寒冰掌 、 火焰刀。

多面编程语言Scala

作者简介: 卢俊祥,网名2gua,译者,书迷;关注Web技术趋势,热衷App开发、Web 开发、数据分析、架构设计以及各类编程语言;陈氏太极拳 五十六式爱好者;佛禅人生,缘散缘聚。 微博:@2gua、 博客 。

(责任编辑:夏梦竹)

本文选自程序员电子版2015年6月B刊,该期更多文章请查看这里。2000年创刊至今所有文章目录请查看程序员封面秀。欢迎 订阅程序员电子版 (含iPad版、Android版、PDF版)。

正文到此结束
Loading...