抽象方法
控制抽象
可复用的代码
所有的函数都被分割成通用部分(它们在每次函数调用中都相同)以及非通用部分(在不同 的函数调用中可能会变化)。通用部分是函数体,而非通用部分必须由参数提供。
当你把函数值用做参数时,算法的非通用部分就是它代表的某些其它算法。在这种函数的 每一次调用中,你都可以把不同的函数值作为参数传入,于是被调用函数将在每次选用 参数的时候调用传入的函数值。这种高阶函数(higher-order function)带其它函数做 参数的函数提供了机会去组织和简化代码。
例子。一个工具类,提供了很多查找文件的方法,有根据文件结尾的、文件名是否包含 指定字串的、文件名是否匹配正则的:
object FileMatcher { // private method, get file name list in current dir private def filesHere = (new java.io.File(".")).listFiles // by file name end with string def filesEnding(query: String) = for (file <- filesHere; if file.getName.endsWith(query)) yield file // by file name end include string def filesContaining(query: String) = for (file <- filesHere; if file.getName.contains(query)) yield file // by file name match regex def filesRegex(query: String) = for (file <- filesHere; if file.getName.matches(query)) yield file }
如果在Java中对应这种情况,大家应该都知道如何提炼接口来重用代码,这里就不啰嗦了。
如果是在某些动态语言中,要提炼一个工具方法提炼出共用的部分,根据传入不同method
作为参数也匹配也很方便,可以直接把代码「拼接」起来:
def filesMatching(query: String, method) = for (file <- filesHere; if file.getName.method(query)) yield file
不过Scala不是动态语言,不能这么拼接。虽然不能把方法名作为参数传递,但可以通过 字面量在运行时产生对应的函数值:
def filesMatching( query: String, matcher: (String, String) => Boolean ) = { for (file <- filesHere; if matcher(file.getName, query)) yield file }
字面量只说明了函数的类型是(String, String) => Boolean
,不用关内部逻辑的。现在
已经有了一个filesMatching
方法来处理共同的逻辑,三个具体的匹配方法只要调用它
就行了:
def filesEnding(query: String) = filesMatching(query, _.endsWith(_)) def filesContaining(query: String) = filesMatching(query, _.contains(_)) def filesRegex(query: String) = filesMatching(query, _.matches(_))
上面的代码可能太抽象了,加上参数表和参数类型可以更加好理解一些:
// _.endsWith(_) (fileName: String, query: String) => fileName.endsWith(query) // _.contains(_) (fileName: String, query: String) => fileName.contains(query)) // _.matches(_) (fileName: String, query: String) => fileName.matches(query))
代码已经被简化了,但它实际还能更短。注意到query
传递给了方法filesMatching
,
但filesMatching
根本用不着这个参数,只是为了把它传回给传入的matcher
函数。
所以在这里可以直接把参数query
绑定到函数字面量中,这样fileMacthing
方法就不要
query
这个参数了。
object FileMatcher { private def filesHere = (new java.io.File(".")).listFiles private def filesMatching(matcher: String => Boolean) = for (file <- filesHere; if matcher(file.getName)) yield file def filesEnding(query: String) = filesMatching(_.endsWith(query)) def filesContaining(query: String) = filesMatching(_.contains(query)) def filesRegex(query: String) = filesMatching(_.matches(query)) }
简化客户端代码
高阶函数可以提供更加强大的API,让客户的代码写起来更加简单。
比如List中的高阶函数exists
方法已经提供了遍历整个集合的抽象,用户只要把判断符合
的函数传入就可以了。下面的两个例子非常简单地实现了检查是否存在负数和是否存在奇数
两个方法:
scala> def containsNeg(nums: List[Int]) = nums.exists(_ < 0) containsNeg: (nums: List[Int])Boolean scala> def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1) containsOdd: (nums: List[Int])Boolean scala> List(1, 2, 3, 4) res1: List[Int] = List(1, 2, 3, 4) scala> containsNeg(res1) res3: Boolean = false scala> containsOdd(res1) res4: Boolean = true
如果没有高阶函数exists
,那就要自己写循环的逻辑,就会有很多重复的代码:
def containsNeg(nums: List[Int]): Boolean = { var exists = false for (num <- nums) if (num < 0) exists = true exists } def containsOdd(nums: List[Int]): Boolean = { var exists = false for (num <- nums) if (num % 2 == 1) exists = true exists }
柯里化(Currying)
理解柯里化可以帮助理解如何建立自己的控制结构。柯里化就是一个函数有多个参数列表。
普通的函数,实现了两个Int型参数,x和y的加法:
scala> def plainOldSum(x: Int, y: Int) = x + y plainOldSum: (Int,Int)Int scala> plainOldSum(1, 2) res4: Int = 3
curry化后的同一个函数,两个列表的各一个参数:
scala> def curriedSum(x: Int)(y: Int) = x + y curriedSum: (Int)(Int)Int scala> curriedSum(1)(2) res5: Int = 3
实际上背靠背地调用了两个传统函数。第一个函数调用带单个的名为x
的Int
参数,并
返回第二个函数的函数值。第二个函数带Int
参数y
。下面的名为first
的函数实质上
执行了curriedSum
的第一个传统函数调用会做的事情:
scala> def first(x: Int) = (y: Int) => x + y first: (Int)(Int) => Int
调用第一个函数并传入1——会产生第二个函数:
scala> val second = first(1) second: (Int) => Int = <function>
从上面的结果可以看出我们得到的结果是一个函数,并把这个函数赋值给了变量second
。
通过second
调用第二个函数传入参数2产生结果:
scala> second(2) res6: Int = 3
first
和second
函数只演示连接在curriedSum
函数上的那两个函数,并不直接连接在
curriedSum
函数上的那两个函数。但我们仍然有一个方式获得实际指向curriedSum
的
「第二个」函数的引用。你可以用偏应用函数表达式方式,把占位符标注用在curriedSum
里
,如:
scala> val onePlus = curriedSum(1)_ onePlus: (Int) => Int = <function>
之前说过,当占位符标注用在传统方法上时,如println _
,你必须在名称和下划线之间
留一个空格。不然编译器会误认为是要调用名为println_
的函数。而在这个例子里不需要
,因为println_
是Scala里合法的标识符,curriedSum(1)_
不是合法的标识符,所以会
被解释为curriedSum(1)
与占位符_
。
现在得到了指向一个函数的引用,这个函数在被调用的时候,传入Int
参数加一并返回
结果:
scala> onePlus(2) res7: Int = 3
由于第二个函数的参数已经有了(传入的是2),现在再用参数2调用第一个函数也能有结果 出来:
scala> val twoPlus = curriedSum(2)_ twoPlus: (Int) => Int = <function> scala> twoPlus(2) res8: Int = 4
通过柯里化强化类型推导
在同一个参数列表中推导不出类型:
scala> def func[A](a: A, f: A => String) = f(a) func: [A](a: A, f: A => String)String scala> func(100, i => s"$i + $i") <console>:9: error: missing parameter type func(100, i => s"$i + $i") ^
不同的参数列表,相关于两个函数。第一个函数已经确定了类型, 第二个函数也就得到类型了:
scala> def func[A](a: A)(f: A => String) = f(a) func: [A](a: A)(f: A => String)String scala> func(100)(i => s"$i + $i") res2: String = 100 + 100
通过柯里化编写新的控制结构
在拥有头等函数的编程语言中,可以在方法中以函数作为参数创造自己的控制结构。
比如有个「重复操作」的方法,它可以把任何操作重复执行两次:
scala> def twice(op: Double => Double, x: Double) = op(op(x)) twice: ((Double) => Double,Double)Double scala> twice(_ + 1, 5) res9: Double = 7.0
如果在工作中曾经遇到重复操作两次(Double) => Double
类型函数的操作的话,这样就把
一个控制结构给抽象出来了。
再考虑一个常用的工作流程:打开一个资源,对它进行操作,然后关闭资源。你可以使用 如下的方法将其捕获并放入控制抽象:
def withPrintWriter(file: File, op: PrintWriter => Unit) { val writer = new PrintWriter(file) try { op(writer) } finally { writer.close() } }
以后要使用的时候就只要传入要处理的文件和处理的方法就行了,打开一个资源和关闭资源 都已经在高阶函数中被抽象出来了:
withPrintWriter( new File("date.txt"), writer => writer.println(new java.util.Date) )
这个技巧被称为贷出模式(loan pattern),因为控制抽象函数,如withPrintWriter
,
打开了资源并「贷出」给函数。当函数完成的时候,它发出信号说明它不再需要「借」的资源。
于是资源被关闭在finally
块中,以确信其确实被关闭,而忽略函数是正常结束返回还是
抛出了异常。
让客户代码看上去更像内建控制结构的一种方式是使用大括号代替小括号包围参数列表。 Scala的任何方法调用,如果你确实只传入一个参数,就能可选地使用大括号替代小括号 包围参数:
scala> println("Hello, world!") Hello, world! scala> println { "Hello, world!" } Hello, world!
这个大括号技巧仅在你传入一个参数时有效,多个参数只能用小括号:
scala> val g = "Hello, world!" g: java.lang.String = Hello, world! scala> g.substring(7, 9) res12: java.lang.String = wo scala> g.substring { 7, 9 } <console>:1: error: ';' expected but ',' found. g.substring { 7, 9 } ^
以前面例子里定义的withPrintWriter
方法举例。在它最近的形式里,withPrintWriter
带了两个参数,因此你不能使用大括号。虽然如此,因为传递给withPrintWriter
的函数
是列表的最后一个参数,你可以使用curry化把第一个参数,File
拖入分离的参数列表。
这将使函数仅剩下列表的第二个参数作为唯一的参数:
def withPrintWriter(file: File)(op: PrintWriter => Unit) { val writer = new PrintWriter(file) try { op(writer) } finally { writer.close() } }
可以用更赏心悦目的语法格式调用这个方法:
val file = new File("date.txt") withPrintWriter(file) { writer => writer.println(new java.util.Date) }
第一个参数列表,包含了一个File参数,被写成包围在小括号中。第二个参数列表,包含了 一个函数参数,被包围在大括号中。
传名参数
上节展示的withPrintWriter
方法不同于语言的内建控制结构,如if
和while
,在于
大括号之间的代码带了参数。withPrintWriter
方法需要一个类型为PrintWriter
的参数
。这个参数以writer =>
方式显示出来:
withPrintWriter(file) { writer => writer.println(new java.util.Date) }
然而如果你想要实现某些更像if或while的东西,根本没有值要传入大括号之间的代码, 那该怎么做呢?为了解决这种情况,Scala提供了传名参数。
为了举一个有现实意义的例子:虽然Scala提供了它自己的assert
,但是用户想自己实现
一个称为myAssert
的断言架构。
myAssert
函数将带一个函数值做输入并参考一个标志位来决定该做什么。如果标志位被
设置了,myAssert
将调用传入的函数并证实其返回true
。如果标志位被关闭了,
myAssert
将安静地什么都不做。 如果没有传名参数,你可以这样写myAssert
:
var assertionsEnabled = true def myAssert(predicate: () => Boolean) = if (assertionsEnabled && !predicate()) throw new AssertionError
用函数字面量的简写方式可以让代码短很多。但函数字面量的简写方式只能用在有参数的
情况下,用占位符_
来代替参数。没有参数也就不能用函数了面量的简写形式。
所以说现在不爽的地方是虽然用不到参数,但调用时却不能省略() =>
:
myAssert(() => 5 > 3) myAssert(5 > 3) // Won't work, because missing () =>
传名函数恰好为了实现你的愿望而出现。要实现一个传名函数,要定义参数的类型开始于
=>
而不是() =>
。如,改() => Boolean
为=> Boolean
:
def byNameAssert(predicate: => Boolean) = if (assertionsEnabled && !predicate) throw new AssertionError
现在可以省略了,看起来像语言内建的控制结构一样:
byNameAssert(5 > 3)
传名类型中,空的参数列表()
被省略,它仅在参数中被允许。没有什么传名变量或
传名字段这样的东西。
对于myAssert
,我们费了这么大的力气,只是为了让函数字面量看起来像表达式,那
为什么不直接用Boolean
变量作为参数呢?
def boolAssert(predicate: Boolean) = if (assertionsEnabled && !predicate) throw new AssertionError
当然这种格式同样合法,并且使用这个版本boolAssert
的代码看上去仍然与前面的一样:
boolAssert(5 > 3)
虽然如此,这两种方式之间存在一个非常重要的差别须指出:表达式会在传入参数前先被 执行。
所以在上面的例子中,如果断言被禁用,你会看到boolAssert
括号里的表达式的某些
副作用,而byNameAssert
却没有。例如,如果断言被禁用,boolAssert
的例子里尝试
对x / 0 == 0
的断言将产生一个异常:
scala> var assertionsEnabled = false assertionsEnabled: Boolean = false scala> boolAssert(x / 0 == 0) java.lang.ArithmeticException: / by zero at .<init>(<console>:8) at .<clinit>(<console>) at RequestResult$.<init>(<console>:3) at RequestResult$.<clinit>(<console>)...
但在byNameAssert
的例子里尝试同样代码的断言将不产生异常:
scala> byNameAssert(x / 0 == 0)
Execute Around Method 模式
举个例子,对于资源Resource
一定要执行善后工作cleanUp
。这里把构造函数做成私有
来限制只能通过伴生对象来构造:
class Resource private() { println("Starting transaction...") private def cleanUp() { println("Ending transaction...") } def op1 = println("operation 1") def op2 = println("operation 2") def op3 = println("operation 3") }
伴生类的中可以创建实例,成员方法use
创建实例并传入到作为参数的回调方法中:
object Resource { def use(codeBlock:Resource => Unit) { val resource = new Resource try{ codeBlock(resource) } finally { resource.cleanUp } } }
使用时传入:
Resource.use { resource => resource.op1 resource.op2 resource.op3 resource.op1 }
调用时的输出:
scala tmp.scala Starting transaction... operation 1 operation 2 operation 3 operation 1 Ending transaction...
动态成员
静态类型不光是变量类型是确定的,还有比如在使用qual.sel
时,sel
这个属性或是
方法(Scala的访问一致性,属性和方法有时候并没有那么大的区别)必须在qual
的
类型中声明了的。
Scala 思考再三还是加入了 Dynamic Types,这个特性在Scala 2.9中是试验性的,
必须用-Xexperimental
进行开启,到了 Scala 2.10.0 中,开启方式有两种:
-
代码中有
import scala.language.dynamics
-
代码中不引入,但编译时加
-language:dynamics
选项。
虽然Scala 2.10.0加进了Dynamic Types特性,但Scala仍然是静态类型的语言,因为在 编译器同样会检查多出来的类型。
有了Dynamic Types之后,Scala又可更DSL了,方法名的动态上可以让它随时包括深刻的 业务含义。相比Java的DSL的能力就太逊了,我们几乎无法在Java面前提DSL这回事。
通俗点讲动态类型的类必须继承自Dynamic
。所有的变化就在下面这四个方法中:
-
selectDynamic
-
updateDynamic
-
applyDynamic
-
applyDynamicNamed
说明:
-
当使用
qual.sel
,而Qual
类未定义sel
属性或方法时,
会调用selectDynamic(method: String)
方法。
-
当
qual.name = "Unmi"
时会调用类似updateDynamic(method: String)(args: Any)
这样的方法。
-
还有
applyDynamic
,applyDynamicNamed
这两个方法的自动调用。
看个完整的例子,我不打算把上面四个方法的应用规则分开来演示:
import scala.language.dynamics class Person extends Dynamic{ def selectDynamic(method: String){ println(s"selectDynamic->$method called\n") } def applyDynamic(method: String)(args: Any*){ println(s"applyDynamic->$method called, args: $args\n") } def updateDynamic(method: String)(args: Any){ println(s"updateDynamic->$method called, args: $args\n") } def applyDynamicNamed(method: String)(args: (String, Any)*) { println(s"applyDynamicNamed->$method called, args: $args") for((key, value) <- args){ println(s"key: $key, value: $value") } } } val p = new Person //calll selectDynamic p.sayHello //call applyDynamic p.config("Hello","Unmi") //call updateDynamic p.products = ("iPhone","Nexus") //call applyDynamicNamed p.showInfo(screenName="Unmi", email="fantasia@sina.com")
上面对p
的每一个调用都说明了会委派给哪个动态方法,执行结果输出是:
selectDynamic->sayHello called applyDynamic->config called, args: WrappedArray(Hello, Unmi) updateDynamic->products called, args: (iPhone,Nexus) applyDynamicNamed->showInfo called, args: WrappedArray((screenName,Unmi), (email,fantasia@sina.com)) key: screenName, value: Unmi key: email, value: fantasia@sina.com
现在来看发生了什么,Person
继承自Dynamic
,并且有引入
scala.language.dynamics
。对p
调用的方法(属性)都不存在,但是都调用到了正常的
动态方法。所以仍然要对这四个动态方法(确切的讲是四种类型的方法,因为比如你可以
定义多个不同的updateDynamic
方法,其余三个也同此) 分别加以说明。
selectDynamic
在调用找不到了无参方法时,会去寻找它,调用效果如下:
p.sayHello
也可以写成p.selectDynamic("sayHello")
。也就是说编译器在看到
p.sayHello
调用会根据selectDynamic(method: String)
。相当于创建了方法
def sayHello = .......
,也就是把动态方法selectDynamic(method: String)
换成
sayHello
。所以说Scala的Dynamic
类中的xxxDynamic
方法相当是模板方法。
applyDynamic
,updateDynamic
和applyDynamicNamed
这三个方法第二个括号中的
参数类型,或个数需根据实际应用来定。这四个动态方法的第一个括号中的参数都是
动态调用时的方法名。
applyDynamic
在进行有参数的方法调用时,会去找寻它,调用效果如下:
p.config("Hello", "Unmi")
可以写成p.applyDynamic("config")("Hello", "Unmi")
还是这么理解: 把这个动态方法定义的方法名和第一个括号与参数替换成调用的方法名 就知道怎么回事,例如把:
def applyDynamic(method: String)(args: Any*)
中的applyDynamic(method: String)
替换成被调用方法名config
,就是:
def config(args: Any*) //p.config("Hello", "Unmi") 要调用的就是这么个方法
所以第二个括号中的参数由你自己来定,比如说想这么调用p.config("Hello", 100, 30)
,那么你可的动态方法可以这么定义:
def applyDynamic(method: String) (greeting: String, high: Int, low: Int) { // ...... }
这个规则同样适用于updateDynamic
和applyDynamicNamed
这两个方法。
updateDynamic
等号赋值操作时会调用updateDynamic
方法,调用效果如下:
p.products = ("iPhone", "Nexus")
可写成:
p.updateDynamic("products")(("iPhone", "Nexus"))
按照同样的理解方法,相当于Person
中定义了def products(args: Any)
方法。
applyDynamicNamed
同样是apply
开头,所以这个方法是对applyDynamic
方法的补充,即使没有
applyDynamicNamed
,单用applyDynamic
也能达成我们的要求。
applyDynamicNamed
只是让你用命名参数调用时方便,也就是像:
p.showInfo(screenName="Unmi", email="fantasia@sina.com")
这样用命名参数的方式来调用动态方法时会调用updateDynamicNamed
方法。有了这个方法
在命名传递参数就方便处理key/value
值。
这四个方法在一个动态类中只能分别定义一个版本,否则会产生二义性,这和普通方法的
重载不一样的。柯里化后的函数第二个括号中的参数可根据实际调用来定义,定义成
(args: Any*)
可包打天下。