Scala精粹

戏路如流水,从始至终,点滴不漏。一路百折千回,本性未变,终归大海。一步一戏,一转身一变脸,扑朔迷离。真心自然流露,举手投足都是风流戏。一旦天幕拉开,地上再无演员。

Scala是由Martin Ordersky设计的一门混合「面向对象」和「函数式」,并具备完备的「泛型编程」能力,支持多种范式的程序设计语言。

Scala取名为「可扩展的语言」,因为它拥有良好的弹性。它使用不变的语言内核,构建万千变化的世界。同时,Scala也是设计「DSL(领域描述语言)」的利器,让编程充满轻松,愉快的气氛,并富有成就感。

Scala拥有强大的类型系统,具有丰富的表达能力,语法简洁,优雅,灵活。它应用广泛,从编写简单的脚本,到构建大型的企业级系统。

Scala运行于JVM之上,并与现有的JVM生态系统无缝链接,而无需定义额外的胶水代码。它兼容既有Java的类库,让成千上万的程序可以继续工作,并能够得以复用。

Scala的哲学

Scala的设计吸收了众多程序设计语言优秀的思想,取精除粕,形成了自身特有的哲学思维体系。

  • 自由:释放自由,方能创造奇迹;
  • 复用:讨厌重复,重用既有代码;
  • 抽象:正交设计,拥抱未来变化;
  • 开放:对外扩展开放,对内拒绝修改;
  • 友好:专家级的瑞士军刀;

Scala的基因

Scala首先偏向Java社区的使用习惯,包括表达式,代码块,还有包和引用的等语法习性。而对于Java用户唯一提出挑战的就是「类型修饰」被放在变量后面了;但是,当习惯了Scala代码风格后,你会发现「后置类型修饰」具有很多优势。

Scala借鉴了Smalltalk的「对象模型」,修正了Java对象模型存在的诸多不足。例如,在没有损失性能的前提下,将AnyVal, AnyRef两者完美统一起来;不仅考虑层次的顶端,还设计了层次的末端,例如,Nothing的抽象,对「类型推演」具有重大意义。

Scala也借鉴了Haskell「类型系统」的设计,及其「函数式」的思维,并结合自身特性,优雅地将OOFP整合在一起,取长补短,极大地增强了Scala的威力。

Scala也借鉴了C++「多重继承」,并吸收了RubyMixin的特性,设计了强大的trait机制实现灵活的对象组合机制。

Scala也借鉴了Erlang的思想,在没有改变内核的情况下,通过扩展类库的方式支持actor的并发编程模式。

Scala也借鉴了C++语言的一些特性,例如「操作符重载」,「隐式转换」等特性;尤其增强了的「隐式转换」成为Scala可扩展性的重要机制。

Scala的特质

接下来,通过几个简单的例子,阐述Scala所具有的一些特点,并阐述选择Scala的动机。

Scala是自由的

No One Size Fits All.

Scala既增强了OO的语义,也引入了FP的思维,同时也拥有完备的「泛型编程」能力,它们互相截长补短,并融为一体。

Scala更像一把瑞士军刀,支持多种编程范式。Scala程序员拥有丰富的工具箱,当面对具体问题时拥有很大的自由空间,力求使用最简单的方法解决问题。

需求:要定义一个「读取器」,可以从字符中直接获取,可以从文件中读取,甚至从网络IO读取。

为了得到一致的抽象,可以定义Reader的抽象体,并对「数据源」这一变化方向进行分离。Scala是一门多范式的程序设计语言,这里尝试使用两种不同的思维尝试解决这个问题。

类型参数

先定义泛型的Reader[+T],并赋予协变的能力。

trait Reader[+T] {
  val source: T
  def read: String
}

然后,再子类化一个StringReader,数据源从字符串中直接获取。

class StringReader(val source: String) extends Reader[String] {
  def read: String = source
}

再定义一个FileReader,数据源从文本文件中获取。

class FileReader(val source: File) extends Reader[File] {
  def read: String = 
    using(Source.fromFile(source)) { file => 
      file.getLines.mkString(Properties.lineSeparator) 
    }
}

using是一个自定义的抽象控制结构,用于保证资源的安全释放,它是「借贷模式」的经典应用,后文将对它进行阐述。

抽象类型

也可以先定义一个抽象类型:type In;基于抽象类型In,再定义了一个抽象字段:val source: In;最后,Reader还定义了一个抽象方法read

trait Reader {
  type In
  val source: In
  def read: String
}

然后,再子类化一个StringReader,数据源从字符串中直接获取。

class StringReader(val source: String) extends Reader {
  type In = String
  def read: String = source
}

如果数据源从文本文件中获取,FileReader实现如下:

class FileReader(val source: File) extends Reader {
  type In = File
  def read: String = 
    using(Source.fromFile(source)) { file =>
      file.getLines.mkString(Properties.lineSeparator) 
    }
}

Scala是抽象的

Scala对于控制系统的复杂度拥有强大的抽象能力。甚至具备「控制结构」的抽象能力,使得设计更加正交,合理,程序更加简单,优雅。

需求1:判断某个单词是否包含数字

使用Java

可以使用迭代快速实现这个需求。

public static boolean hasDigit(String word) {
  for (int i = 0; i < word.length(); i++)
    if (Character.isDigit(word.charAt(i)))
      return true;
    return false;
}

需求2:判断某个单词是否包含大写字母

当然,可以通过复制粘贴,重复实现相同的逻辑;但是将导致明显的重复设计。

public static boolean hasUpperCase(String word) {
  for (int i = 0; i < word.length(); i++)
    if (Character.isUpperCase(word.charAt(i)))
      return true;
    return false;
}

为了得到更为抽象的设计,使得代码具有高度的可复用性,可以提取一个抽象的CharacterSpec概念。

public interface CharacterSpec {
  boolean satisfy(char c);
}

hasDigit, hasUpperCase合二为一,实现算法逻辑的代码复用。

public static boolean exists(String word, CharacterSpec spec) {
  for (int i = 0; i < word.length(); i++)
    if (spec.satisfy(word.charAt(i)))
      return true;
    return false;
}

可以如下使用这个函数,判断单词是否包含数字。

exists(word, new CharacterSpec() {
  @Override
  public boolean satisfy(char c) {
    return Character.isDigit(c);
  }
});

对于判断是否包含大写字母,则可以实现为:

exists(word, new CharacterSpec() {
  @Override
  public boolean satisfy(char c) {
    return Character.isUpperCase(c);
  }
});

但是,使用匿名内部类,将导致复杂的程序结构和冗余的语法噪声。

使用Java8

可以使用Java8lambda表达式简化设计。

exists(word, c -> Character.isDigit(c));

如果使用「方法引用」,可以进一步改善程序的表达力。

exists(word, Character::isDigit);

但是,即使使用Java8,设计依然还是美中不足。其一,exists拥有两个参数,如果能够做到如下的「代码块」,那就太神奇了。

// 假设可以定义代码块
exists(word) { Character::isDigit };

其二,如果将exists成为String的一个方法,设计将更加具有OO的风格了。

// 假设String拥有exists方法
word.exists(Character::isDigit);

可惜的是,Java并没有上述的能力。

使用Scala

首先,Scala可以兼容既有的Java设计,而无需付出额外的成本。按照惯例,对Java实现的StringUtil.exists可以做一个简单的包装,隐藏匿名内部类的实现细节,并对外提供「代码块」定制的风格。

def exists(s: String)(p: Char => Boolean): Boolean =
  return StringUtil.exists(s, new CharacterSpec {
    override def satisfy(c: Char): Boolean = p(c)
  })

也就是说,相比Java的实现,Scala借助「柯里化」的机制,进一步改善代码的表达力。

exists(word) { _.isUpper }

事实上,Scala运用「隐式转换」的神奇魔法,可以将String的功能增强为StringOps,使其直接能够调用exists方法。

word.exists(_.isUpper)

如果偏爱大括号,也可以写成这样:

word.exists { _.isUpper }

Scala是简洁的

Scala极度讨厌「重复」,严格坚持DRY(Don't Repeat Youself)原则。不仅体现在语法上,还包括类库的设计上。

需求:设计一个货币的值对象。

使用Java

Java实现「值对象」时,语法较为啰嗦,并具有相同的模式,很容易形成重复的「样板代码」。

例如使用private定义字段,并在构造函数进行初始化;然后定义字段的Getter接口;即使equals,hashCode等具有逻辑的方法时,也表现为固定的模式。

public class Currency {
  private final int amount;
  private final String designation;

  public Currency(int amount, String designation) {
    this.amount = amount;
    this.designation = designation;
  }

  public String getDesignation() {
    return designation;
  }

  public int getAmount() {
    return amount;
  }

  @Override
  public int hashCode() {
    ...
  }

  @Override
  public boolean equals(Object obj) {
    ...
  }
}

Java社区,也存在实用方法,或者类库,支持equals, hashCode的自动生成,但也要让程序员付出额外的努力。

使用Scala

Scala对于重复的事情从来不说两次。对于固定的模式,拥有最直接、最简洁的表达方式,从而大幅地削减了代码量。

case class Currency(amount: Int, designation: String)

Scala社区,case class是定义「值对象」的最佳实现模式。它天然地拥有Getter, equals, hashCode等方法实现,并且在其「伴生对象」中拥有apply的工厂方法。

Scala是性感的

Scala语法轻量,并具备丰富的表达力。借助于Scala强大的「类型推演」能力,Scala的简洁程度可以和「动态语言」相媲美。

需求:建立一个电话簿的数据表格。

使用Java

使用Java建立一个简单的电话簿数据表格。

Map<String, String> phonebook = new HashMap<String, String>() {{
  put("Horance", "+9519728872");
  put("Dave", "+9599820012");
}};

其中,参数类型<String, String>重复地声明了两次,构造静态表也使用了特殊的「初始化」的语义。

使用Scala

使用Scala,代码实现不仅轻量,表现力也相当不错。

val phonebook = Map(
  "Horance" -> "+9519728872"
  "Dave"    -> "+9599820012"
)

没有冗余的类型噪声,而且具有更形象的语法描述。更重要的是,->操作符并不是语言内核所支持的,而是通过简单地类库扩展而实现的。

也就是说,"Horance" -> "+9519728872"构造了一个类型为Tuple2[String, String]的二元组,它等价于("Horance", "+9519728872")

事实上,->定义在Predef中。

object Predef {
  implicit final class ArrowAssoc[A](self: A) extends AnyVal {
    def ->[B](y: B) = (self, y)
  }
}

ArrowAssoc实现了self: A的功能增强,使其拥有->方法,而该方法返回一个二元组。

Scala是多变的

Scala犹如变形金刚,拥有无穷变化的空间。Scala倡导一个问题,拥有多种解法的思维习惯。当面对具体问题时,使得程序员拥有更多的自由选择权。

需求:打印程序选项列表

使用var

Scala虽然倡导函数式的思维,但在某些性能苛刻的场景,指令式可能成为最后的救命稻草。

object Main extends App {
  var i = 0
  while (i < args.length) {
    println(args(i));
    i += 1
  }
}
for推导式
object Main extends App {
  for (arg <- args)
    println(arg)
}
函数字面值

Scala的函数具有一等公民的地位,跟其他值一样可以被传递和存储。foreach就是一个高阶函数,它接受函数字面值作为参数进行传递。

object Main extends App {
  args.foreach((arg: String) => println(arg))
}
类型自动推演
object Main extends App {
  args.foreach(arg => println(arg))
}
占位符

因为argforeach中仅出现过一次,可以使用_的占位符简化实现。

object Main extends App {
  args.foreach(println(_))
}
部分应用函数

也可以将println看成一个整体进行传递。需要注意的是,println_之间有且仅有一个空格。

object Main extends App {
  args.foreach(println _)
}

对于此例,可以将传递println函数名,简化实现,提高表现力。

object Main extends App {
  args.foreach(println)
}
可选的逗号和括号

在特殊场景下,逗号和括号是可选的,代码的表现力犹如自然语言一样直白。

object Main extends App {
  args foreach println
}

Scala是开放的

Open for Extension.

Scala具有高度可扩展性的架构,借助于trait,抽象类型和泛型,Self Type,隐式转换等机制,使得它具备强大的灵活性。

using的设计为例,讲解Scala对于扩展性的支持,及其设计内部DSL的技术,加深对Scala的理解。

形式化

对于资源管理,可以简化为如下的数学模型:

Input: Given resource: R
Output:T
Algorithm:Call back to user namespace: f: R => T, and make sure resource be closed on done.

它表示:给定一个资源R,并将资源R传递给用户空间,并回调算法f: R => T;当过程结束时资源自动释放。

using实现
import scala.language.reflectiveCalls

object using {
  def apply[R <: { def close(): Unit }, T](resource: => R)(f: R => T): T = {
    var source: Option[R] = None
    try {
      source = Some(resource)
      f(source.get)
    } finally {
      for (s <- source)
        s.close
    }
  }
}

using常常被称为「借贷模式」,是保证资源自动回收的重要机制。

控制抽象

使得using形如内置于语言的控制结构,其行为类似于if, while一样。

def read: String = using(Source.fromFile(source)) { file =>
  file.getLines.mkString(Properties.lineSeparator) 
}
鸭子编程

R <: { def close(): Unit }用于约束R类型的特征,它必须拥有def close(): Unit方法。这是Scala支持「鸭子编程」的一个重要技术。

例如,File满足R类型的特征,因为它拥有close方法。

惰性求值

resource: => R是按照by-name传递,在实参传递形参过程中,并未对实参进行立即求值,而将求值推延至resource: => R的调用点。

例如,using(Source.fromFile(source))并没有马上调用fromFile方法,并传递给形参,而将求值推延至source = Some(resource)语句,即调用Some.apply方法时才展开计算。

Option

Scala社区,Option常常用于表示「存在与不存在」两者之间的语义。它存在两种形式:Some, None

for推导式

finally关闭资源时,使用for推导式过滤掉None。也就是说,如下三种形式是等价的。

  • 过滤掉None,并自动提取Option中的元素。
for (s <- source)
  s.close
  • 使用if,但需要从Some中手动get
if (source != None)
  source.get.close

Scala的明天

软件设计的目的就是为了控制复杂性,让软件应对未来变化具有更好的弹性。Scala强大而自由,当程序员设计一个应用和类库时,具有很大的自由空间。

但是,Scala过度的灵活性,往往会诱惑他人掉进复杂性的深渊而不能自拔。它犹如具有「魔戒」的力量,虽然强大,但也很致命。

Complexity is like a bug light for smart people. We can't resist it, even though we know it's bad for us. -- Alex Payne.

因此,应该理智地抵制复杂性的诱惑,才能真正地发挥Scala的威力。使用Scala不是为了炫技,而应该尽最大的可能让设计保持简单。

Martin Ordersky也在2016年元旦之初发文,号召社区有志之士在未来的时间里尽最大可能地降低Scala的复杂度。

我坚信,Scala的明天会更简单,更加漂亮。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容