Jade Dungeon

抽象方法

控制抽象

可复用的代码

所有的函数都被分割成通用部分(它们在每次函数调用中都相同)以及非通用部分(在不同 的函数调用中可能会变化)。通用部分是函数体,而非通用部分必须由参数提供。

当你把函数值用做参数时,算法的非通用部分就是它代表的某些其它算法。在这种函数的 每一次调用中,你都可以把不同的函数值作为参数传入,于是被调用函数将在每次选用 参数的时候调用传入的函数值。这种高阶函数(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 

实际上背靠背地调用了两个传统函数。第一个函数调用带单个的名为xInt参数,并 返回第二个函数的函数值。第二个函数带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 

firstsecond函数只演示连接在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方法不同于语言的内建控制结构,如ifwhile,在于 大括号之间的代码带了参数。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)

这样的方法。

  • 还有 applyDynamicapplyDynamicNamed这两个方法的自动调用。

看个完整的例子,我不打算把上面四个方法的应用规则分开来演示:

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方法相当是模板方法。

applyDynamicupdateDynamicapplyDynamicNamed这三个方法第二个括号中的 参数类型,或个数需根据实际应用来定。这四个动态方法的第一个括号中的参数都是 动态调用时的方法名。

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) {
	// ...... 
}

这个规则同样适用于updateDynamicapplyDynamicNamed这两个方法。

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*)可包打天下。