Coding Poet, Coding Science

Scala编程概要(四):控制抽象, 集合, 模式匹配, 编译器

Scala的控制抽象

Haskell有自己的抽制抽象。使用的是单体。但是在Scala中,没有必要做得那么纯函数式。所以为了调用控制流,Scala使用了() => Unit类型。比如说,我们要在某个线程当中创建一个过程:

runInThread() { () => println("Hi"); Thread.sleep(1000); println("Bye") }

其中定义的runInThead函数是:

def runInThread( block : () => Unit) {
    new Thread {
        override def run() { block() }
    }.start()
}

注意,由于可以使用大括号代替小括号,所以我们能够看到,调用runInThread,就好像是在过程体中写出一系列的函数一样。

实现runInThread的更简洁的形式,应该是这样的:

runInThread {
    println("Hi")
    Thread.sleep(10000)
    println("Bye")
}

相对于上面的带有明显的高阶函数构造过程的实现,只有一点困难,就是我们如何省略掉() =>。Scala实际上鼓励后者的这种方式,称为是换名调用。为了实现上述调用代码,对runInThread的定义变成:

def runInThread(block : => Unit) {
    new Thread {
        override def run() { block }
    }.start()
}

显然,定义的时候省略了参数()就可以做到。这样的话,可以构造出很多类似于控制流的语句。这样以来,看起来我们可以定义很多自己的控制流,比如do..untilrunInAnotherMachine {}这样的控制语句。带参数的控制流当然也是可以定义的,因为我们可以与柯里化的函数结合使用。实际上,scala的if语句就是通过这里实现的。

until的实现见如下的代码:

def until(condition : => Boolean) (block: => Unit) {
    if (!condition) {
        block
        until(condition) (block)
    }
}

在上面的代码中,除了柯里化函数定义外,我们尤其应当注意,为了使每次until的时候都能对condition求值一次,我们必须使condition惰性化。所以,condition是() => Boolean这样的形式。调用的时候,使用

var x = 10
until (x=10) {
    x -= 1
    println(x)
}

不过,上面的语句中,仍然没有做到在until的第一个体里面声明变量x,而是得在until的外面声明变量,这种用法也不是完全的漂亮。

最后是Scala函数中return的使用。return确实是显式地终止函数。但是为了使用return,就相当于破坏了Scala的类型返回推导。因此,我们在定义有return的函数的时候,必须指定返回的类型。(其实应该还是能推导出来,但是可能没有通用的方案)。编程语言如果实现太高级,那么程序员的习惯可能难以适应(除了Haskell这样的语言)。

Scala的集合操作

相当一部分语言处理的任务是在列表、元组、字典这样的数据结构中实现的。它们就是一组数。在数学上看来自然是没有不同的。但是在编程中,似乎不同的方法需要单独处理一下。而且面对复杂的操作可能,也需要合适的抽象。

Scala的任何一种集合(列表、元组、字典)要么是可变的,要么是不可变的。而且所有类型的集合,都可以不用使用new方法来创建。因为它们都有apply方法。不可变的集合是非常理想的,因为在多线程的应用程序中使用他们也不会造成问题(不可变数据结构在单线程中的优势可能不明显,但是在多线程中,这种设计的优势是显而易见的)。

Scala优先采用不可变的集合类型。内置的List、Set、Map也都是不可变的。为了使用可变的数据结构,必须导入scala.collection.mutable包,以便使用mutable.Map得到可变的映射。(以及使用Map得到不可变的映射)。

不可变的序列Seq被定义为一个trait,而这个特质被许多类进一步采用。比如Vector、Range、List、Stream、Stack、Queue。Vector的优点是支持随机的访问。向量是树形的结构实现,向量中的元素每个节点可以有不超过32个子节点。这样访问100万的元素的向量,只需要跳转四下。但是在链表中访问某个元素却比较复杂(线性访问)。

试想象如下的代码

Range(0,1000000000)(100000000)

在这样的代码中,我们要首先生成一个序列(当然,Range其实是惰性的,只在有需要的时候才生成)。但是我们要想取得某个位置的元素的值,如果是Iterable的话,还是需要迭代到相应的值。这样,我们定义Range当然没有问题,但是要访问后面的值的时候,就得逐个向前走。特别是,如果Iterable的对象是一个File流,那么原则上,访问第\(n\)个元素的时候,也访问了之间的元素。因此Seq的朴素的实现,效率并不高。

Scala的可变序列也继承自Seq这个trait。具体的成员有IndexedSeq、ArrayBuffer、Stack、Queue、PriorityQueue、LinkedList、DoubleLinkedList。(回忆之前我们讲过,Scala对于递归函数、默认参数、带名参数、变长参数都有支持)。

集合的操作类型有很多。而且Scala也引入了对它们的模式匹配。所以是一个比较大的课题(大概Python中的集合类型也可以作为编程实践中的一大部分)。

在可变类型结构中,有时候我们会使用list(3)=5这样的方法。注意,Scala没有为集合类型重新定义一个[]的操作符,而且也没有必要。在Scala当中,这样的元素具有apply方法,所以可以直接应用它来进行赋值的操作。

Java与Scala的集合类型的相互转换,以及互操作这里也不介绍了。目前是没有什么收益的。

另外,即使有不可变的集合,大多数的并发编程的任务还是要求比较高级的线程安全特性。在Scal中,引入了相应的集合的Synchronized版本,比如SynchronizedSet。在并发库中,提供了ConcurrentHashMap等类。而且,相应的也有并行的版本。比如

for (i <- (0 until 100).par) print(i+" ")

这里,通过一个par方法把循环变成并行的版本。

注:七周七并发中提到了多种并发的模型,而《程序设计语言实践之路》中提到了创建线程的多种语法(比如,有的使用begin块来实现并行的代码,有的是通过并行循环,有的是加工时启动Ada。fork/join,隐含接收与早回复的模式也是实现并行常见的)。基本上这六种包含了在语言设计中实现并发或并行的所有的使用模式。不同的编程语言支持的模式也不相同。

Scala的模式匹配与样例类

Scala的模式匹配的机制是比较强大的。具体地说,模式匹配可以用于match、类型检查,获取变量,匹配表达式类型。可以在模式匹配中添加守卫,使用通配符等。

模式匹配可以匹配变量的值,是也是最基本的方式。也可以用于匹配一个类型。

给模式添加守卫的方法示例如下:

ch match {
    case '+' => sign = 1
    case '-' => sign = -1
    case _ if Character.isDigit(ch) => digit = Character.digit(ch,10)
    case _   => sign = 0

第三句就是加入了一个守卫。注意,守卫出现在动作的前面。另外,模式匹配是从上到下执行的,遇到成功的匹配就跳出余下的匹配。

注意Scala的命名模式。Scala的变量以小字字母开始。大写字母开头的符号表示的是一个常量。如果一个常量以小写字母开头,那么必须在使用的时候前后加上左单引号。

表达式的类型也可以参与匹配:

obj match {
    case x : Int => x
    case s : String => Integer.parseInt(s)
    case _ : BigInt => Int.MaxValue
    case _ => 0
}

这里相当于添加了类型限定符。模式匹配在语法上是比较一致的。但是因为匹配的变量、常量、类型、实例类的不同,有些语句可能在编译器就决定了,有些可能被推迟到生成运行期的匹配的代码。

模式匹配与apply/unapply的用法之间已经介绍过了。但是模式匹配还可以用于变量的声明,以及用在for循环当中。所有这些,都应当视为学习模式匹配的重点。

偏函数

在模式匹配语句没有在所有的情况下有定义的时候,很容易导致偏函数。在Scala中,任何一个偏函数,类型是PartialFunction[A,B]。偏函数就是不在所有的位置都有定义的函数。使用偏函数的时候,如果使用得不恰当,会自动抛出异常。

Scala的注解特性

目前了解到,注解可以以不同的方法实现就可以了。而且在Scala中,注解可以为类、方法、字段、局部变量、参数、表达式、类型参数以及各种类型定义添加注解。这样的多种多样的注解,可能导致我们无法简单地使用高阶函数来统一理解注解。

Scala不同的一点在于,Scala的注解可以影响编译的过程。通过注解,可能往类里面自动地添加一些方法。在Scala中,注解是annotation.Annotation的扩展。而注解可以有不同的类型,如StaticAnnotationClassfileAnnotation等。

Scala的注解广泛用于和Java的互操作、优化执行。以及添加额外的检查,实现条件编译等。

Scala的泛型,类型参数

Scala的类、特质、方法、函数都可以有类型参数。类型参数放在名称的后面,用中括号括起来。可以使用类型界定<:等。

类型界定的重点集中在隐式类型转换与自动类型推导上。

Scala的类型系统还是比较复杂的。包括类与特质、元组类型、函数类型、带注解的类型、参数化类型、单例类型、类型投影、复合类型、中置类型、存在类型等。但是一般而言,这么多类型强调的是对于不同情况的适应,而非建立类型系统的一般的框架。所以我们先跳过对于Scala的类型系统、类型运算关系的介绍。但是实现依赖注入、抽象类型等的时候,我们可能必须返过来仔细阅读有关Scala类型系统的介绍。

与面向对象中的继承、多态(特别是多态)混合的时候,类型系统变得更复杂。

学过Haskell我们就知道类型构造器与数据构造器。实际上,Scala语言也有这样的概念。不过,相关的概念放在编译器特性当中了。这使得我们在学习Scala编译器系统的时候才能比较全面地看待Scala的类型系统。

Scala的解析器库

Scala的解析器库是编写内部DSL的一个非常好的应用(注意,内部DSL虽然是离不开宿主环境的,但是仍然是从字符串中读取内容,否则就不是DSL了)。

Scala的DSL的实现非常优雅,得益于它的操作符系统,以及正则表达式匹配。原则上我们也可以用操作符机制来自己写解析器库,但是Scala的解析器库提供了一个高级的选择。

学习解析器生成自然是每个编程者的目标,但是目前自己的困境是没有理解DSL领域的一般的实践的规则。也就是说,写DSL有哪些实践,又有哪些应用,哪些应用是常见的。我们不是任意地写一个解析器,然后生成一个不成熟的实践。

Scala的编译器[09-11-2015 23:07:38 CST]

现在Scala的编译器是github上面的一个项目。在Scala项目中,Scala语言本身占了很多。其实是5.9%的Java语言和少部分的Python与Shell语言。

另外,安装sbt请参考http://www.scala-sbt.org/0.13/tutorial/Installing-sbt-on-Linux.html