Coding Poet, Coding Science

Scala编程概要(三):进程控制与shell,函数式特性

应该说,这种特性是可以让人大吼大叫的。简单的示例如下:

import sys.process._
"ls -al .." !

之所以可以这样做,是因为sys.process包含一个从字符串到ProcessBuild对象的隐式类型转换。注意后面的!号操作符,以ProcessBuild类型为参数类型,返回的是被执行程序的返回值。如果需要返回的是程序的输出的字符串,可以使用!!代替!号。

如果需要把程序通过管道,scala支持使用#|运算符。比如:

"ls -al .." #| "grep sec" !

这样显然具有优势,因为让scala可以编译检查程序的结构。除此之外,scala的运算符还有重定向#>#>>#<等。如下面的示例:

import sys.process._
"ls -al .." #>> new File("output.txt") !
"grep Scala" #< new URL("http://horstmann.com/index.html")

这种特性确实是不错,貌似比Shell还要更强大一些。

注意!表示返回的结果,是一个整型的值而已。另外,Scala中的流程控制语句有p #&& q表示p成功则执行q,p #|| q表示p不成功则执行q。(一个规则是,相对于原来的shell操作符,我们只需要在前面加上一个#号就是scala脚本的操作符,而且与原来的shell操作符具有相同的优先级。

至于正规的方法,则是构造相应的Process对象,然后使用!操作符执行它。比如

val p = Process(cmd, new File(dirName), ("LANG", "en_US"))
"echo 41" #| p !

有兴趣的话,可以参考一下。因为scala提供了一个快速构建器fsc,所以shell的执行的速度也不是问题。

Scala中的正则表达式

正则表达式分析使用scala.util.matching.Regex类。在scala中,可以使用"""..."""作为原始的字符串,里面的符号不进行转义(与Python还有一些不同)。

val numPattern = "[0-9]+".r
/* 构造正则表达式对象,使用字符串的.r方法
 * 该方法是在scala.util.matching.Regex中定义
*/
val wsnumwsPattern = """\s+[0-9]+\s+""".r

//然后使用findAllIn, findFirstIn等方法来匹配字符串
val m1 = wsnumwsPattern.findFirstIn("99 bottles, 98 bottles")

//或者使用replace来替换字符串
val m2 = num.replaceFirstIn("99 bottles, 98 bottles", "xx")

为了使正则表达式有提取的功能,可以使用PCRE中的字符串分组。比如

val numitemPattern = "([0-9]+) ([a-z]+)".r
val numitemPattern(num, item) = "99 bottles"
num == "99"
item == "bottles"

这样,编程实践中的系统调用接口、正则表达式,文件IO、XML我们就都已经提到了。

Scala中的特质

相当于java中的interface,但是进行了一些哲学的修正。比如使用with,表示的含义是“一个类与其它的特质运算,得到新的类,然后由某个类来继承它”。

但是在scala中,特质可以包含具体的实现的方法。java的接口是不能做到这一点的。但是求是地看,让特质具有具体的行为之后,如果特质发生了改变,所有的混入了该特质的类都必须重新编译。

特质在处理日志、异常处理的时候也占有一席之地,甚至还是一种不错的解决办法。另外一种解决日志与异常处理的任务的方法是使用高阶函数。虽然直观上,显然高阶方法与注解器更为高级(其中高阶函数更高级)。由此我们大概可以抽象出异常处理的几种编程的模式,或者甚至是理论。

如果想要使用特质来处理日志与异常处理,那么就需要仔细阅读Scala中有关特质、特质扩展类、自身类型(self type)的一些知识点。

Scal中的操作符[07-02-2015 10:26:53]

摘自《快学Scala》中的教程。对于Haskell之类的操作语言,理解操作符的使用是很自然的事情。因为在这样的语言中,计算被移到语言的更后端的层次,而在C语言一类的命令式语言中,操作符被当成是编译器语法的一部分,直接写在语言里。显然是后者更好。但是这两种方式的不同,也给学习新的语言造成了一些障碍。

Scala中的操作符的使用与语法的解析是没有关系的。我们参考Scala的语法的手册就可以知道。所以操作符的使用带来了比较大的灵活性。

Scala中的标识符与操作符的重载是实现内置领域特定语言的一个重要的方式。前面我们提到了sys.process._也可以看成是DSL的一类。

在Scala中,命名采取统一的规则,变量,函数、类的名称都统称为标识符。编译器不再对标识符作进一步的语法使用上的区分。在Scala中,选择标识符是比较自由的。还可以使用unicode,或者其它的非空白符。

在Scala中,可以作为操作符的字符是相当多了,除了一些语法上的禁忌之外。比如三种括号已经有特定的含义了,我们不能重载为操作符。但是苦寒经的ASCII字符与Unicode的数学符号都可以作为操作符(注意不是所有的Unicode符号,只有Unicode的数学符号这一类)。

另外,Scala使用左单引号括起来的部分作为一个单独的标识符(就像latex的\csname ... \endcsname一样。这使得在单引号里面,几乎所有的字符都可以当成是标识符。

在Scala中,任何一个带有两个参数的方法都可以写成是a 标识符 b的形式。Scala也知道这是一个二元方法调用。要注意的是,中间的方法必须以a.method(b)的形式声明。所以a op b表示op是a的一个方法,这个方法的参数是b。

另外,由于操作符也可以使用->这样的名子,所以就实现了操作符的方法调用。

类似于二元操作符的是后置的操作符,后置的操作符实际上是一个类的无类的方法。比如1 toString。这两种约定是自然的语法糖特性,没有什么特殊的。

值得注意的是对于前置一元操作符的处理。前置的一元的操作符实际上调用的是相应的unary_op方法。比如-a实际上调用的是a.unary_-方法。在操作符中,有些以=号结尾的操作符具有特定的赋值的含义,所以我们不应该在操作符中使用=号,除非我们在语义上有赋值区需求。当然,我们实际上可以自定义具有=号的操作符,scala也会认识相应的方法。但是一般而言,还是尽可能保持等号的特殊的含义。

在Scala当中,操作符的优先级是预定义好的,由操作符的首字母来决定,有些语言,如OCaml当中,操作符的优先级的次序是我们可以自己指定的。但是这种方法在Scala中不适用。除了赋值操作符之外,Scala的优先级由操作符的首字母来决定。也就是按照各个字符的列表来确定操作符。操作符有三类,中置操作符、后置操作符,前置操作符。统一的规定是,中置操作符优先级高于后置的。

在具有相同的优先级的时候,操作符的结合性才发挥作用。在Scala中,用户定义的所有的操作符都是左结合的操作符(大多数代数运算均如此)。而在内置语言中,以冒号结尾的操作符以及赋值操作符是右结合的。这样,自然而然地,构造列表的::操作符是右结合的。

对于右结合的二元操作符a rop b,调用的方法是b.rop(a)。即方法属于后一个类,不是属于前一个类。

函数调用方法扩展

语法特性f(arg1, arg2, ...)不只是用于直接定义的函数的调用与语法。它们也可以用于函数之外的值。除了函数与方法调用之外,这种形式的语法还被当成是f.apply(arg1,arg2)的语法糖。而f(arg1, arg2, ...) = value这种形式,还对应于如下调用f.update(arg1, arg2, ..., value)。即这种语法是一个语法糖。Scala在遇到这样的符号的时候会自动查询函数调用、方法调用,以及apply方法。

为什么需要使用apply方法?答案是,在Java当中,创建新的对象都要使用new方法。但是这种方法对于不变对象而言是非常不好的。所以,一般而言,都在类的伴生对象中定义一个apply方法,这个apply方法的实现是new ClassName(args)这样的形式。(这样可以直接使用ClassName(args)来构造一个类的对象。(另外,实例类也具有类似的效果)。使用apply方法来构造一个对象,可以看成是Scala的一个设计模式。

unapply方法,看起来很违背常识。但是在编程语言的世界中,尤其是函数式语言的世界中,一个apply方法就是一个把参数变成对象的一个映射(其实是附带算子的),所以,自然而然地,我们考虑参数的逆映射,也就是把一个对象分解成构造它的时候的参数。unapply方法在模式匹配的时候特别有用。比如,我们要匹配对象Fraction(a,b)的时候,必然需要从对象中找到ab的值。这个时候就需要unapply方法(从而我们知道,模式匹配并不是什么神奇的东西,本质上就是一个方法调用,这个方法调用把一个类变成相应的值)。当然,模式匹配不仅要从对象中解出来值,还要检查对象的类型。但是至少从对象中提取出来值这部分,是通过unapply方法完成的。

注:这里我们对模式匹配要做的工作有了进一步的认识。但是同时要指出,模式匹配的时候,还是要处理额外的一些问题,比如模式匹配如果失败了怎么办?一般而言,这需要一种处理不确定性的方法。比如,返回的值可能是Maybe(3,4)这样的形式(在Scala中,是Some((3,4))这样的形式)。

注:Scala的实例类case class自动地生成apply与unapply方法。实际上,Scala的unapply还被用于实现其它的许多的功能。比如说,赋值语句的模式匹配,如val Number(n) = "1729",可以把整数1729的值赋给n。等等。详细可以参考《快学Scala》当中的第十一章操作符的最后一节。

注:使用提取符实际上已经可以构造一个表格的DSL,就像LaTeX那样。所以也许我们该来试验一下。比如在表格里面通过运算符重新构造出HTML或者LaTeX格式的表格。

下面是自己写的一个测试用例,允许用户使用Table() | "abc"这样的形式构造表格。

class Table(var elem : String) {
    def |(element : String) = new Table(elem + "&" + element)
    def ||(element: String) = new Table(elem + "\n"+element)
}
object Table {
    def apply(e:String) = new Table(e)
    def unapply(input: Table): Option[String] = Some(input.elem)
}

var t = Table("abc") | "String" 
println(t.elem)

t match {
    case Table(e) => println("Succeed: " +e) 
    case _ => println("failed")
}

注意,第一点是把伴生对象中定义apply与unapply方法。第二是unapply的返回类型是Option[TYPE],而且返回的值要用Some(VALUE)代替。这样才能得到正确的值。自己是尝试写这样的代码,肯定还有一些缺陷。等以后再来写一个完整的表格分析工具。

总结:本小节尝试的是操作符与模式匹配的构建。

高阶函数的使用

其实是这一种思维方式的训练。函数式语言编程中广泛使用这些方法。具体来说的模式有:把函数作为值来返回;需要的时候直接定义一个表达式作为函数;以及使用通配符来实行柯里化;函数式语言的控制抽象等。其中有一些课题明显是所有的高级语言的共性,比如说控制抽象。

Scala的高阶函数的机制引入了类型推导的概念。不然我们在构造高阶函数的时候,将写出大量的类型声明代码。

匿名函数的语法是(x : Doube) => 3 * x。注意使用=>来表示计算(因为不同的语言实现\(\lambda\)-函数的方式都有所不同。可以把这个函数赋给一个变量。变量自然是有类型的。如果有可能,我们还可以让一个变量指向不同的函数。

高阶函数在编程语言中,往往被看成是轻量级的函数。第一个是作用域可以非常局部,比如说,直接在map的参数当中生成,第二是其意义往往只有一次,比如说,让一个数组增加三倍,并不值得先单独写一个乘以3的函数,然后慢慢传过去。

注意在语法上Array(3,4).map((x:Double) => 3 * x)的效果,可以用Array(3,4).map{ (x: Double) => 3 * x}以及Array(3,4) map { (x:Double) => 3 * x}来代替。至少使用大括号使得层次更加明显。另外,其实这里的(x:Double) => 3 * x可以换成更简单的3*_这个匿名函数。

在数值计算中,高阶函数的使用也有一些意义。比如我们有一组函数,需要求出一组函数在某个点处的值。这个时候使用map当然是最直观的。但是使用

val valueAt = (f: (Double) => Double, x : Double) => f(x)
valueAt(3.0+_, 1.0)

这样的形式显然更为简单。

注:上式中我们还了解了多参数的匿名函数的语法,以及把函数作为参数的用法。如果有可能,我们还可以写出f at 1.0这样的中缀的形式。是否是很吸引人呢?

常见的高阶函数

很多的高阶函数都应用的集合类型上面。常见的有map、foreach、filter、reduceLeft等。这里在处理数据的时候我们再来了解。

《快学Scala》的作者还提到了闭包。不过,闭包是任何的编程语言的结构体的共同的特性。本质是如果变量没有在这个过程体中定义,或者是在调用的上下文中并没有原来的变量的值的时候,在过程体中的变量的值该如何绑定。比如说,定义函数

def scale(x) : x*factor

当中,实际上factor没有出现在函数体中。那么factor一般是一个外部变量。但是关键的是,如果编译器遇到它的时候,在要执行scale的时候,factor该怎样取得值呢?这种问题就是所谓的闭包。

当然,在scala的匿名函数完成是可以使用闭包的。调用的时候,闭包内的变量被绑定到调用的时候的作用域所确定的同名子的变量。这也是闭包的标准的使用策略。基本上任何语言都是这样。

闭包应该属于编程理论中的“名子,作用域”中讨论的一个概念。它并非是函数式语言所特有的。但是在函数式语言中,闭包的使用更加重要。

沟通函数式语言与命令式语言的编程任务

我们在函数式语言中,知道函数可以像变量一样赋值,这样可以完成非常强大的任务。但是对于那些函数式不是一等公民的语言中,怎么样才能完成同样的任务呢?

非面向对象的语言有自己的方法。比如C语言可以通过指针把函数传递过去。但是考虑到封装的要求,这样也许不是一个好的办法(虽然C语言也支持源文件级别的封装等。

面向对象的语言的通行的做法,根据《快学Scala》中的介绍,是这样的,为了把一个函数参数传给另外一个函数或者对象,我们得把相关的动作方法放在一个实现某个接口的类当中,然后将该类的一个实例传递给另一个方法。当然,这种方法与函数式语言通用性相比还存在一些缺陷。比如在Java中,类是由编译期确定的,所以我们不能在运行期创建新的函数。仅仅为了封装单个方法而设计的接口我们称为是SAM(single abstract method)类型。(这也是Java程序员的叫法)。

在事件驱动与GUI上面,比如为了使按钮在被点击的时候递增一个计数器,我们使用:

var counter = 0
val button = new JButton("Increment")
button.addActionListener(new ActionListener {
    override def actionPerformed(event: ActionEvent){
        counter += 1
    }
})

这当然是一段比较长的代码。而且非常冗余。因为我们只是要让计数器加一这个函数传递过去而已。一个比较合理的做法是这样的:

button.addActionListener((event : ActionEvent) => counter +=1)

我们注意到,面向对象的语言中,很少提到动态创建那些有新的方法的未知的类。动态类型的语言中能够做到这一点,自编译的语言也可以。但是似乎面向对象的实践中从来没有认真考虑过在运行的时候创建具有新的方法与成员的类。

在上面的转换代码中还应当注意,在函数式语言中添加一个函数是完全没有问题的。但是Scala有自己的情况:要使得代码能够与Java代码互操作。所以,在Scala中,为了启用上面的语法,还是需要提供一个隐式类型转换函数。示例如下:

implicit def makeAction(action: (ActionEvent)=> Unit) = 
    new ActionListener {
        override def actionPerformed(event: ActionEvent) { action(event)
}

这样做的实际结果,还是使得在函数式的代码在编译的时候,被转换成一个新的类。(不过,这个类似乎是在可以运行的时候动态创建的)。

柯里化函数

在Haskell语言中,所有的函数都是自动柯里化的。Scala是函数式的语言,自然应当提供对于柯里化支持。在Scala中,柯里化的函数有自己的定义的语法,比如:

def mul(x: Int)(y:Int) = x*y

柯里化函数式多个参量的函数的互换也经常使用(柯里化与反柯里化)。在一些需要传递高阶函数的地方,经常需要从已有的函数构造柯里化函数。如前所说,我们已经使用_来构造柯里化函数了。

至于Scala中有没有反柯里化,自己现在不太清楚。但是原则上,Scala应该禀承函数式语言的一贯的思维,认为多参数的函数根本不是编程语言中什么根本性的特质。我们完全可以把多参数的函数看成是一个单参数的函数的多次应用,正如我们在数学当中看到的那样。只提供多参数的函数的参数的一部分,是非常自然的数学思维,也应该成为编程的一种思维。

《快学Scala》中给出了一个常见的编程的实践:我们想测试两个序列在给定的某个对比的条件下是否是相同的。这里使用的是corresponds函数。使用示例是一个判断字符串序列在忽略大小写的意义下是否是等同的。

val a = Array("hello", "world")
val b = Array("Hello", "World")
a.corresponds(b)(_.equalsIgnoreCase(_))

分析这个编程任务,我们知道,本质上还是两个对象之间的比较,所以应该有一个二元的函数。但是我们又需要在某个标准下面才能得到结果。因此,二元运算不是得到true或者false,而是还得接受一个提供判别标准的函数。所以,corresonds的类型是:

def corresponds[B] (that: Seq[B]) (p:(A,B) => Boolean) : Boolean

定义了这样的一个柯里化的函数,这种设计既保持了语义的自然,实现起来也比较简洁。但是注意,这里的corresponds是Array[B]类别中预定义的函数,所以使用a op b这样的形式是没有问题的。但是,如果我们想使两个Double型的变量具有a add b这样的调用形式,那么原则上我们要为Double类添加一个add方法,但是这可能又要修改Scala的库,所以可能是不能实现的。(因为要保持这个类,同时又能为类单独添加新的方法,似乎是一种矛盾)