首先,Scala中的几乎所有的语言元素都是表达式。也就是返回一个值。另外,Scala中的val实际上是“常量”,或者说,“字面变量”的意思。相当于给常量一个名子。对于编程实践与软件工程具有重要性。
函数可以用def语句定义也可以使用val语句来定义。主要是看有没有小括号方法。这一点就很容易混少淆了:def与val定义的函数在实质上有什么区别呢?
def square(a: Int) = a * a
def squareWithBlock(a: Int) = { a * a}
val squareVal = (a: Int) => a * aScala的变量的使用的时候不需要指明类型,而且Scala支持按值传递与按名传递,所以就有了不同的模式。试着把函数定义为 def log(msg: String) 与把函数定义为 def log(msg: => String) 都不会使 log(MSG + 1 / 0 ) 出现编译期错误。然而后者是按名传递,所以1/0不会被求值,也就不会在运行期出现错误。按名传递的优点是可以避免不必要的计算。
标准的调用类的方式是使用new方法。
柯里化currying的意思是允许一个函数的多次调用,并且只有最后一次调用的时候返回的是正常的值,中间的所有的调用只是产生一个特定函数(参数被确定了)。
Traits就像是一个Interface。但是可以有函数体。
模式匹配特性
自己中间的时候接触到了模式匹配。模式匹配与switch case语句最大的匹别就是它可以直接匹配一个类型而不单单是匹配在一个值。在支持高阶函数等特性的语言中,模式匹配是很强大的,尤其是用来构造一个分析器。
def fib(int: Any) : Int = in match {
case 0 => 0
case 1 => 1
case n: Int => fib(n-1) + fib(n-2)
case _ => 0
case n: String => fib(n.toInt)
}注意上面的后两个匹配条件。它显然容许了final类型的匹配,以及在泛型的情况下,如果匹配到一个字符串的类型,就把它处理成整数后再计算。这对于应用程序传参是非常有利的。应该是比具有变长参数的函数传递,以及Key=Value方法的传递更自由。
实例类
一个函数与一个类有什么样的区别呢?在函数的调用的时候,以及类的构造的时候,都是使用所谓的小括号的表达式。但是不同之处在于,给类传递参数的时候,类的参数会持久化起来。因为类有存储空间。这样的话,类的参数的信息实际上是保留下来了。于是我们可以认为类是一个惰性的函数。即使被调用的时候,里面的参数也保存了下来。请看下面的写法:
abstract class Expr
case class FibExpr(n : Int) extends Expr {
require(n >= 0)
}
case class SumExpr(a: Expr, b: Expr) extends Expr
def value(in: Expr): Int = in match {
case FibExpr(0) => 0
case FibExpr(1) => 1
case FibExpr(n) => value(SumExpr(Fib(n-1), Fib(n-2)))
case SumExpr(a,b) => value(a) + value(b)
case _ => 0
}
println(value(FibExpr(3)))工作的时候,FibExpr(0)作为一个参数被匹配。因为FibExpr是一个实例类,所以实例类中的成员都保存了下来。在语义上,FibExpr出现在Case中,表示该类的名称是FibExpr,同时它的传值是0。正好匹配。这样的话,一个类的对象更像是一个实体。因为我们可以拿两个类的构造函数直接比较。
因为函数式语言可以直接将函数作为传递的参数。同时函数式语言中函数的声明与操纵往往很方便,所以判断也是很容易的。比如Scala中|list.exists(_ % 2 ==1)|就表示判断列表当中有没有奇数。
函数式语言中的Map-Reduce
在Scala中实现Map和Reduce是非常直接的。因为List对象直接有map方法,而且map的参数就是一个函数。另外,map后可以直接使用reduceLeft这样的函数。用于把列表折叠起来。假设StrList是一个包含字符串的列表,那么下面的方法就用MapReduce实现了一个统计列表中的单词总数的方法:
def wordCount(str: String): Int = str.split(" ").count("msg"==_)
file.map(wordCount).reduceLeft(_+_)Map的参数是一个正常的,操纵单个元素的函数是很常见的。许多向量式的语言就有这个特性。但是我们对于reduce函数却生疏一些。因为reduce要把许多元素结合起来。所以reduce函数的执行有两个要素,第一个是怎么结合子元素,第二是怎样执行合并的次序。reduceLeft函数的参数告诉我们,从左到右,两个元素依次相加,返回的元素再和右边的元素相加,直到最后一个元素被处理。
Scala还具有优化尾递归的能力。因为在函数式语言中,尾递归的效率比C++之类的要高很多。在编译器就会全部优化这些东西。
Scala的for也是函数式的for。意思是依次读取列表中的元素,生成一个新的列表。在for当中使用yield可以返回值。比如:
val counts = for (line <- file) yield wordCount(line)
val num = counts.reduceLeft(_+_)总而言之,Scala对于多种编程方式都有支持,它们完成的任务都类似。关键还是看风格的问题了。现在我们缺的不是先进的编程方法,而是如何更好地运用这样的编程方法。
模式匹配的应用范围很广。比如在得到一个值的时候,返回的是错误该怎么办?通常情况下就要进行错误处理了。但是这样其实很麻烦。更好的做法,而且不打断业务逻辑的做法就是使用内容匹配。
处理错误通常发生在同样具有意义的函数的高层。通常而言是由某个操作所引起的。因此都是放在try块当中。使用错误处理的时候,利用match方法更为简洁,这是因为,处理错误的代码与算法的代码被分割了。比如
val osName = getProperty("os.name")
osName match {
case Some(value) => println(value)
case _ => println("none")
}
println(osName.getOrElse("none"))
osName.foreach(print _)Scala的lazy初始化也是一个具有重要意义的特性。其重点在于把耗时的操作推后,放在尽可能需要做的时候才开始做。特别是访问网络。比如一个类需要访问网址的时候,最好的方法可能不是在构造类的时候就开始做,而是首次访问网络的时候。这个时候用lazy操作就可以了。另外,lazy操作中调用lazy函数的时候,操作也不会立即完成。只有lazy变量被首次使用的时候,才逐步执行lazy变量的初始化。
Scala的并发
Scala的并发主要是Actor库。Actor是Scala的并发模型。而2.10之后,推荐Akka作为Scala的Actor的实现。Actor类似于一个线程,有一个邮箱。Actor可以通过system.actorOf来创建,通过receive来获取消息,用叹号发送消息。实现了语言级别对于并行的支持。Actor还有一个DSL可以使用。Actor是比线程还较量的并发的实现。因为Scala可以实现线程的复用。
Scala通过future实现异步的返回。另外,对于列表,Scala还能够形成并行集合。像普通并行计算那样进入并行区。通过par.map方法就可以实现并行的map操作了。不过,这里的并行,主要是利用多核的能力。
Akka也支持远程的并发的模型,在Akka中通过akka:<//RemoteSystem@127.0.0.1>:2552/user/echoServer这样的地址就可以做到分布式的消息通信与并发。
自己可能在C42的第一阶段中了解编译的概念与并发的概念,并将它们与操作系统相结合起来了。这样好像是一切的基础。而且自己觉得,只有软件工程与安全工程、系统工程学习得多了,才知道编译器为什么这样设计、并行系统为什么那样处理。
个人始终认为,学习计算机和学习数学是一样的道理,自己学习过很多的基础的知识,未必就理解它的意义和用途了。反而是自己把更高级的东西学会之后,有了新的视角的时候,对于有些东西才能看得非常透。比如,编译原理与操作系统和网络显然是非常深的知识,但是我们肯定不知道为什么这样。
实战中的Scala(与Java的互操作)
首先是Java与Scala的互操作。在scala中有beans这样的库,以至于可以允许Scala中也使用beans的对象,Scala也支持beans中的@BeanProperty注解。
在Java中判断类是否相等是一件不容易的事,因为它的equals方法与 == 方法的含义是不一样的。而且有时候这样的判断非常难写正确。在Scala中 == 与equals的含义完全相同。另外,在Scala中,建议在需要判断类的相等的时候使用实例类case class,而不用对通常的类判断相等。
抽取器也是Scala的一大特色。具体来说,抽取器允许直接变量的匹配。注意到Scala的case可以匹配一个带有形参的函数,那么,从Email中抽取用户可以这样做:
import scala.util.matchingRegex
object Email {
def unapply(str: String) = new Regex("""(.*)@(.*)""")
.unapplySeq(str).get match {
case user :: domain :: Nil => Some(user, domain)
case _ => None
}
}
"user@domain.com" match {
case Email(user, domain) => println(user + '@'+ domain)
}这种方法,依赖于对象有unapply方法,以及正则表达式的match功能。
另外,Scala执行中可以启用Cache的功能。具体来说,就是对于函数的指定调用启用Cache。因为每次结果相同,下一次的时候就不再调用了。这种方法需要显式使用。尤其是自己建立一个维护cache的字典。不过,使用的方法也是相当简单的。使用cache的最大的优势可能在于非尾递归的情形下,也不用我们手工改成迭代的方法。在scala中,是通过memo函数(scala.collection.mutable.WeakHashMap)实现的。中文译为“记忆模式”。
记忆模式在计算Fibnacci数之类的问题上是非常显著的。这使得Scala在科学计算中具有表达十分简洁、效率又十分高的特点。
定义了implicit的函数可以成为一个隐式类型转换函数。比如
implicit def strToData(str: String) = ...}这样就可以实现隐式类型转换了。关键还是在于implicit关键字。
另外,Scala的强大的地方就在于DSL。Scala有许多特性看起来是语言级的,但是其实是DSL的结果。比如Scala操作JSON的方法。
此外,Scala还有一些测试的套件。可以使用Spec2、ScalaTest等测试。通过使用shound, mustEqual等语句,就好像是在写自然语言一样(好像是声明式的效果)。
另外,Scala还有自己的sbt工具。构建的时候会方便很多。使用sbt,甚至只需要安装JRE,scala就能够把东西构造出来。
SBT教程
sbt的入门是非常简单的。创建一个新目录,新目录下有一个scala文件,直接运行sbt run,或者先输入sbt,在交互式窗口中输入run,就可以编译并运行结果了。
但是通常情况下,sbt会像java那样工作。也就是,先查找当前目录下的文件,然后查找src/main/scala或者src/main/java目录下的文件,再找src/test/scala与src/test/java目录下面的文件,再找src/main/resources或者src/test/resources目录下的文件。最后找在lib目录下的jar文件。其实是继承了java的优点。因为java就是让哪些不懂软件工程的人也能够按照软件工程的实践来配置,从而避免了随意性。
sbt是和scala同时发布的。默认的情况下,sbt编译的时候也会使用和scala相同的版本的编译器。其实,scala本身就是用java写的编译器。
sbt的配置文件是当前目录中的build.sbt文件。配置根元素的方法是:
lazy val root = (project in file(".")).
settings(
name := "hello",
version := "1.0",
scalaVersion := "2.11.4"
)sbt文件的书写规范可以参考http://www.scala-sbt.org/0.13/tutorial/Basic-Def.html。在project/build.properties文件里面可以加上sbt.version=0.13.8来配置所使用的编译器。scala的工程的目录结构,一般和maven保持相同。但是java目录的优先级要低于scala目录。以点号开头的所有的文件都会被忽略。
在sbt中,主要的配置文件放在build.sbt文件中,而其余的配置文件放在project/目录的下面。它们各自有自己的作用。在project/目录下还可以有.scala文件。它们也用于指导构建的过程。
在目录下完全可以使用.gitigonre文件。尤其是,在根目录下应当有.gitignore文件,把target/目录给忽略掉。
在所有的配置中,添加依赖可以说是最基本的了。一般而言,采取这样的风格:
val derby = "org.apache.derby" % "derby" % "10.4.1.3"
lazy val commonSettings = Seq(
organization := "com.example",
version := "0.1.0",
scalaVersion := "2.11.4"
)
lazy val root = (project in file(".")).
settings(commonSettings: _*).
settings(
name := "hello",
libraryDependencies += derby
)特别是,使用derby常量定义一个串,然后在settings的libraryDependencies中添加这个库。
Play等网页框架
这里介绍了Play框架用来开发Scala网页应用的技术https://playframework.com/ 。这里是中文的一个介绍play框架的博客http://www.cnblogs.com/nixil/articles/play_with_scala.html 。
http://stevenshe.ca/ 是一个博士生的主页。做的也是SPLC与feature model方向。而且使用的编程语言是scala。
Scala的语言的完整的规范,包括语法解析规则可以参考http://www.scala-lang.org/files/archive/spec/2.11/ 。
Scala对于XML的支持
Scala的语言的完整的规范,包括语法解析规则可以参考http://www.scala-lang.org/files/archive/spec/2.11/。以下就是从这本书里面提取出的Scala的语言级别的特性。
XML可以直接作为一种数据类型而出现,而不必添加其它的标注。比如:
val b = <book>
<title>The Scala Language Specification</title>
<version>{scalaBook.version}</version>
<authors>{scalaBook.authors.mkList("", ", ", "")}</authors>
</book>其中的scalaBook.version可以用于对于scala变量的求值。这样一来,XML完全可以作为一种原生的数据类型出现。至少表现得与原生数据类型是一样的。在词法的级别,Scala也引入了其它的一些特性,比如支持unicode作为变量名。这是Scala的词法文法的介绍http://www.scala-lang.org/files/archive/spec/2.11/01-lexical-syntax.html。
在Scala当中,类型、值、方法、类都统称为一个实体(Names in Scala identify types, values, methods and classes which are collectively called entities)。引入一个名称称为绑定(bind)这个名称。在scala当中,引入一个名称的方式有局部声明与定义、继承、import子句、package子句。将一个名称引入之后,scala就知道这个东西该怎样被解析了。
如果不同的绑定定义了同样一个方法,那么它们会有不同的优先级。在编译的时候,编译器会按照优先级来查找这些名称的含义。
在scala中,有两种name spaces。也就是有两个命名的空间。第一个空间是为类型准备的,第二个是为项准备的。因此即使name相同,如果它们属于不同的语法单元,scala也不会把它们弄混了。根据上下文,一个名称,可以被指派给一个类型或者一个项。
每一个绑定都有一个作用域。(A binding has a scope)。在scala中,作用域是可以嵌套的。如果在上下文菜单中出现了一个普通的标识符,scala会尝试从当前生效的作用域中找出这个绑定对应的名子。如果没有这个名子,将会出错。标识符有定域和非定域之分(identifier: unqualified identifier and qualified identifier)。定域的标识符,指的是使用a.x这样中间有点号限定的标识符。
如果是定域标识符,也就是 \(e.x\) 的形式,那么scala会把x绑定到具有成员 \(x\) 的一个对象 \(e\) 当中,但是这个时候, \(e\) 的类型 \(T\) 不是任意选择的。如果 \(T\) 不是value type,那么就会出错。
Scala的类型系统
Scala的类型有value type, non-value type之分。因为对于高阶的支持,所以在scala中有first-order types与type constructors之分。顾名思义,两种类型中,一种是语言内置的,另外一种是接受类型参数以生成另外一种类型。其实可以算是元类型,因为它接受类型参数之后才变成一个具体的类型。
value types is a subset of first-order types. value types又有具体类型concerte与抽象类型abstract之分。它是一个具体的value types的属性。一个具体的value type,要么是具体的,要么是抽象的。value types在scala中又称为first-class values。译为一等值?
Every concrete value type can be represented as a class type, i.e. a type designator that refers to a class or a trait 1, or as a compound type representing an intersection of types, possibly with a refinement that further constrains the types of its members.
Abstract value types are introduced by type parameters and abstract type bindings. Parentheses in types can be used for grouping.
Non-value types capture properties of identifiers that are not values. For example, a type constructor does not directly specify a type of values. However, when a type constructor is applied to the correct type arguments, it yields a first-order type, which may be a value type.
Non-value types are expressed indirectly in Scala. E.g., a method type is described by writing down a method signature, which in itself is not a real type, although it gives rise to a corresponding method type. Type constructors are another example, as one can write type Swap[m[_, _], a,b] = m[b, a] , but there is no syntax to write the corresponding anonymous type function directly.
在scala中,non-value types包含了其它的一些东西。特别是,在scala中,一个方法的类型与它的字面意思并不相同。方法的签名自然是与它的类型有关,但是方法的名称却不能唯一决定它的类型。也就是说,non-value types的含义实际上就是和编程的时候出现在代码中的名称不一样的类型。它的类型是由编译器推导出来的,有一个内部的,隐式的表达。另外,一个类型构造子也不算一个具体的类型,是因为,一个类型构造子的名称还不足以成为一个类型。只有它接受了特定的参数之后,才成为一个类型。
Scala的名称还可能有点号作为限定符。这个时候,完整的路径并不是类型的一部分。但是它们对于确定出现的一个名称是什么类型非常重要。Paths are not types themselves, but they can be a part of named types and in that function form a central role in Scala’s type system.
在scala中的c.x中,x必须是p的一个稳定的成员: \(p.x\) where \(p\) is a path and \(x\) is a stable member of p. Stable members are packages or members introduced by object definitions or by value definitions of non-volatile types.其中,稳定成员就是在路径中,它是一个叶结点: A stable identifier is a path which ends in an identifier.也就是出现c.x之后,x不能再有子成员?
注意,引用一个类的超类中的成员也是可能的。比如C.super.x或者super[M].x。
A stable type is either a singleton type or a type which is declared to be a subtype of trait scala.Singleton.
编程语言的类型问题
在这一领域有许多正式的名称。它们甚至是可以形式地定义出来的。比如\(T[T_1,\cdots]\) 。这个完整的整体叫做一个含参类型,称为parameterized types,而 \(T\) 本身则称为一个类型构造子。也就是说, \(T[T_1]\) 是对类型构造子已经使用了 \(T_1\) 构造之后的结果。类型参数是可以定义上界与下界的。尤其是在声明一个类型构造子的时候。
比如声明 class S[K<: String] {...}; class G[M[Z<:I],I]{...} 的话,可以使用 \(G[S,String]\) 来构造一个带参类。
Scala还允许元组类型。这种类型是由 \((T_1,\cdots,T_n)\) 构成的。在使用的时候,相应的数据的各个维数属于不同的类型,类似于结构体。
另外一种是所谓的注解类(Annotated types),用法如String @suspendable。其它的,还有合成类型(compound types)、中缀类型(Infix Types)、函数类型(function types)等。
函数的类型是由 \(T_1,\cdots, T_n \Rightarrow U\) 来决定的。也可以使用复合的类型,如 \(S\Rightarrow T \Rightarrow U\),但是这个类型是右结合的,表示的是等价于 \(S\Rightarrow (T\Rightarrow U)\)的元素。
中缀类型具有 \(T_1 op T_2\) 的类型。从实现上看,中缀类型是对于\(op[T_1, T_2]\) 的一个语法糖。
在Scala当中,函数是具有apply方法的类,因此函数类型实际上是一种类类型(class types)。函数类型的结果是共变的,但是参数是反变的。
在编程语言中,把一个名子与一个类型关联起来的过程称为绑定。然而,具体实现绑定的方法就是通过声明或者定义。
Scala的Object实际上是由设计模式中的“单件”改变而来。参考 http://zh.wikipedia.org/wiki/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F 。
模式匹配的技术与模式识别正好相反。在模式匹配中,模型是精确指定的。但是在模式识别中,模式却南大要挖掘。Tree patterns are used in some programming languages as a general tool to process data based on its structure, e.g., Haskell, ML, Scala and the symbolic mathematics language Mathematica have special syntax for expressing tree patterns and a language construct for conditional execution and value retrieval based on it. For simplicity and efficiency reasons, these tree patterns lack some features that are available in regular expressions.
使用Scala实现一个符号微分的程序
类型的匹配其实就是使用带参类与泛型的结果。尤其是对于一个表达式,通常的情况下,都是设置优先级来匹配一个类。比如
def Diff(Expr, Var) = Expr match {
case Sum (l, r) => Diff (l, Var) + Diff (r, Var)
case Prod (l, r) => Diff (l, Var) * r + l * Diff(r, Var)
...
}实现中缀形式的匹配也是有可能的,特别是在scala中中缀也可以惰性匹配的情况下。个人认为,大学的时候学习编程语言不能非常方便地写出一个符号微分的程序就是一个失败。
写符号微分的另外一种方式是使用语法解析器。如果能把语法解析器自动嵌入到一个语言中,会是很有趣的一件事情。特别是如果一个语言的子解析模式是可以由用户方便地定制的话。
Scala下的Actor[06-09-2015 22:21:56]
使用AKKA并发库来解决问题。Actor系统的原理我们已经知道了。对于Erlang而言,程序本身就是一个Actor(进程)。而在scala当中,进程通过创建actor中的一个对象实现与actor通信系统的关联。示例的代码如下:
case class Greet(name: String)
case class Praise(name: String)
case class Celebrate(name: String, age: Int)
class Talker extends Actor {
def receive = {
case Greet(name) => println(s"Hello $name")
case Praise(name)=> println(s"$name, you're amazing")
case Celebrate(name, age) => println(s"Here's to another $age years, $name")
}
}
object HeloActors extneds App {
val system = ActorSystem("HelloActors")
val talker = system.actorOf(Props[Talker], "talker")
talker ! Greet("Huey")
talker ! Praise("Dewey")
talker ! Celebrate("Louie", 16)
Thread.sleep(1000)
system.shutdown
}在上面的代码中,使用ActorSystem进入Actor系统当中(可能需要一些认证),而使用actorOf来创建一个actor。发送消息使用叹号,而使用shutdown来实现断开actor.基本原理就是这样。如果需要多个消费者协调,或者错误处理功能,就需要再参考相关的书籍了。