Scala精粹

作者: 刘光聪 | 来源:发表于2016-08-14 13:37 被阅读739次

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

    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的明天会更简单,更加漂亮。

    相关文章

      网友评论

      本文标题:Scala精粹

      本文链接:https://www.haomeiwen.com/subject/ylpysttx.html