减少代码重复

所有的函数都被分割成通用部分,它们在每次函数调用中都相同,以及非通用部分,在不同的函 数调用中可能会变化。通用部分是函数体,而非通用部分必须由参数提供。当你把函数值用做参数时,算法的非通用部分就是它代表的某些其它算法。在这种函数的每一次调用中,你都可以把不同的函数值作为参数传入,于是被调用函数将在每次选用参数的时候调用传入的函数值。这种高阶函数:higher-order function——带其它函数做参数的函数——给了你额外的机会去组织和简化代码。

高阶函数的一个好处是它们能让你创造控制抽象从而使你减少代码重复。

例如,假设你正在写一 个文件浏览器,并且你想要提供一个 API,能够允许使用者搜索匹配某些标准的文件。它提供的功能如下:

1)搜索文件名结束于特定字串的

2)基于文件名的任何部分做查询

3)基于正则 表达式搜索

如果没有代码复用,三个函数将分别如下实现:

object FileMatcher {
private def filesHere = (new java.io.File(".")).listFiles def filesEnding(query: String) =
for (file <- filesHere; if file.getName.endsWith(query))
yield file def filesContaining(query: String) =
for (file <- filesHere; if file.getName.contains(query))
yield file def filesRegex(query: String) =
for (file <- filesHere; if file.getName.matches(query))
yield file
}

由程序可见,三段代码大部分是相同的,是可以通用的,区别仅在于for的过滤表达式的那个函数不同。

你希望能做的的是这样的:

def filesMatching(query: String, method) =
for (file <- filesHere; if file.getName.method(query))
yield file

这种方式在某些动态语言中能起作用,但 Scala 不允许在运行期这样粘合代码。那么你该做什么 呢?

函数值提供了一个答案。虽然你不能把方法名当作值传递,但你可以通过传递为你调用方法的函数值达到同样的效果。

在这个例子里,你可以给方法添加一个 matcher 参数,其唯一的目的就是 针对查询检查文件名:

package chapter9

object FileMatcher2 {

  private def filesHere = (new java.io.File(".")).listFiles

  def filesMatching(query: String,
matcher: (String, String) => Boolean) = {
for (file <- filesHere; if matcher(file.getName, query))
yield file
}
}

方法的这个版本中,if 子句现在使用 matcher 针对查询检查文件名。更精确的说法是这个检查不 依赖于 matcher 定义了什么。现在看一下 matcher 的类型。它是一个函数,因此类型中有个=>。 这个函数带两个字串参数——文件名和查询——并返回布尔值,因此这个函数的类型是(String, String) => Boolean。

有了这个新的 filesMatching 帮助方法,你可以通过让三个搜索方法调用它,并传入合适的函数 来简化它们:

package chapter9

object FileMatcher2 {

  private def filesHere = (new java.io.File(".")).listFiles

  def filesMatching(query: String,
matcher: (String, String) => Boolean) = {
for (file <- filesHere; if matcher(file.getName, query))
yield file
} def filesEnding(query: String) =
filesMatching(query, _.endsWith(_))
def filesContaining(query: String) =
filesMatching(query, _.contains(_))
def filesRegex(query: String) =
filesMatching(query, _.matches(_))

这个例子中展示的函数文本使用了前一章中介绍的占位符语法,对你来说可能感觉不是非常自然。 因此,以下阐明例子里是如何使用占位符的。用在 filesEnding 方法里的函数文本_.endsWith(_), 与下面的是一回事:

(fileName: String, query: String) => fileName.endsWith(query)

原因是 filesMatching 带一个函数,这个函数需要两个 String 参数,不过你不需要指定参数类型。因此,你也可以写成(fileName, query) => fileName.endsWith(query)。由于第一个参数, fileName,在方法体中被第一个使用,第二个参数,query,第二个使用,你也可以使用占位符 语法:_.endsWith(_)。第一个下划线是第一个参数,文件名的占位符,第二个下划线是第二个参数,查询字串的占位符。

代码已经被简化了,但它实际还能更短。注意到query传递给了filesMatching,但filesMatching 没有用查询做任何事只是把它传回给传入的 matcher 函数。这个传来传去的过程不是必需的,因 为调用者在前面就已经知道了 query 的内容。你可以同样从 filesMatching 和 matcher 中简单地 去除 query 参数:

  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))
}

最近的例子里面用到的函数文本_.endsWith(query),包含一个绑定变量, 下划线代表的参数,和一个名为 query 的自由变量。仅仅因为 Scala 支持闭包才使得你可以在最 近的这个例子里从 filesMatching 中去掉 query 参数,从而更进一步简化了代码。

简化客户代码

高阶函数的另一个重要应 用是把它们放在API里使客户代码更简洁。Scala的集合类型的特定用途循环方法提供了一个很好的例子。

考虑 exists,一个判断传入的值是否包含在集合中的方法。当然你也可以初始化一个 var 为假, 循环遍历集合类型,检查每个元素,并且如果你找到了要寻找的就把 var 设置为真,通过这样的 方式寻找元素。以下是使用了这种方式的方法去判断是否传入的 List 包含了负数的例子:

object Demo1 {

  def containsNeg(nums: List[Int]): Boolean = {
var exists = false
for (num <- nums)
if (num < 0)
exists = true
exists
} def main(args: Array[String]): Unit = { val ints = List(1, 2, 3)
println(containsNeg(ints))
}
}

不过更简洁的定义这个方法的方式是通过在传入的 List 上调用高阶函数 exists,通过代码复用,还可以检测奇数:

    ints.exists(_ > 0)
ints.exists(_ %2 == 0)

Scala 的标准库中还有许多其他循环方法。如果你能发现使用它们的机会,那么像 exists 一样, 它们经常能缩短你的代码。

Curry化

curry 化的函数被应用了多个参数列表,而不是仅仅一个

下面代码展示了一个规整的,未被 curry 化的函数,它实现两个 Int 型参数,x 和 y 的加法:

scala> def plainOldSum(x: Int, y: Int) = x + y
plainOldSum: (Int,Int)Int
scala> plainOldSum(1, 2)
res4: Int = 3

相对的,下面的代码展示了 curry 化后的同一个函数。代之以一个列表的两个 Int 参数,你把这个 函数应用于两个列表的各一个参数。、

scala> def curriedSum(x: Int)(y: Int) = x + y
curriedSum: (Int)(Int)Int
scala> curriedSum(1)(2)
res5: Int = 3

这里发生的事情是当你调用 curriedSum,你实际上背靠背地调用了两个传统函数。第一个函数调用带单个的名为 x 的 Int 参数,并返回第二个函数的函数值。第二个函数带 Int 参数 y。下面的名为 first 的函数实质上执行了 curriedSum 的第一个传统函数调用会做的事情:

scala> def first(x: Int) = (y: Int) => x + y
first: (Int)(Int) => Int

在第一个函数上应用 1——换句话说,调用第一个函数并传入 1——会产生第二个函数:

scala> val second = first(1)
second: (Int) => Int = <function>

在第二个函数上应用 2 产生结果:

scala> second(2)
res6: Int = 3

first 和 second 函数只是 curry 化过程的一个演示。他们并不直接连接在 curriedSum 函数上。尽管如此,仍然有一个方式获得实际指向 curriedSum 的“第二个”函数的参考。你可以用偏应 用函数表达式方式,把占位符标注用在 curriedSum 里,如:

scala> val onePlus = curriedSum(1)_
onePlus: (Int) => Int = <function>

curriedSum(1)_里的下划线是第二个参数列表的占位符。2 9.4 编写新的控制结构 结果就是指向一个函数的参考,这个 函数在被调用的时候,对它唯一的Int参数加一并返回结果:

scala> onePlus(2)
res7: Int = 3

然后以下是你如何获得对唯一的 Int 参数加二函数的方式:

scala> val twoPlus = curriedSum(2)_
twoPlus: (Int) => Int = <function>
scala> twoPlus(2)
res8: Int = 4
 
scala中柯里化的一种典型案例——correspondsf方法的柯里化应用
    在scala中corresponds方法能使得两个序列按照一定的条件进行比较,该方法其实也是经过柯里化参数的。在scala的API中该方法定义如下:在方法签名描述中有两个参数,that序列和p函数,其中p函数有两个参数,第二个参数类型是与that序列一致的。应用柯里化,我们可以省去第二个参数中B的类型,因为从that序列中推断出B的类型,第一个参数中U的类型,也可以通过调用者本身推断出来。由于第一个参数在第一个位置,第二个参数在第二个位置,所以两个参数类型都可以省略掉,简写如下:
    def corresponds[B](that: GenSeq[B])(p: (T, B) ⇒ Boolean): Boolean
    因此假如有字符串类型序列a和字符串序列b,我们可以使用a. corresponds(b)(_. equalsIgnoreCase(_))来判断两个字符串序列在不区分大小的情况是否一致,这就为编程带来了“魔法般”的灵活性。   
def main(args: Array[String]): Unit = {

    val a = Array(("abc"),("def"))
val b = Array(("ABC"),("Def"))
println(a.corresponds(b)(_.equalsIgnoreCase(_)))
}

打印结果为true

补充:柯里化为什么有助于类型推断呢?add(x:Int)(y:Int),那么参数x是作为一个自由变量传入第二个函数的

叫名参数:by-name parameter

3 3 你只能称其为 myAssert,而不是 assert,因为 Scala 提供了它自己的 assert,将在 14.1 节描述。 myAssert函数 将带一个函数值做输入并参考一个标志位来决定该做什么。如果标志位被设置了,myAssert将调 用传入的函数并证实其返回true。如果标志位被关闭了,myAssert将安静地什么都不做。 如果没有叫名参数,你可以这样写 myAssert:

  var assertionsEnabled = true
def myAssert(predicate: () => Boolean) =
if (assertionsEnabled && !predicate())
throw new AssertionError def main(args: Array[String]): Unit = { myAssert(()=>5>3)
}

这个定义是正确的,但使用它会有点儿难看。

叫名函数恰好为了实现你的愿望而出现。要实现一个叫名函数,要定义参数的类型开始于=>而不 是() =>。例如,你可以通过改变其类型,“() => Boolean”,为“ => Boolean”,把 myAssert 的 predicate 参数改为叫名参数。

var assertionsEnabled = true
def byNameAssert(predicate: => Boolean) =
if (assertionsEnabled && !predicate)
throw new AssertionError def main(args: Array[String]): Unit = { byNameAssert(5>3)
}

现在你可以在需要断言的属性里省略空的参数了。

本章展示给你如何基于 Scala 的丰富的函数支持建造控制抽象。你可以在你的代码中使用函数提 取通用的控制模式,并且你可以利用 Scala 库里的高阶函数去复用所有程序源代码中都常见的控 制模式。本章还展示了如何使用 curry 化和叫名参数以便你自己的高阶函数能以一种简洁的语法 形式使用。

scala编程(九)——控制抽象的更多相关文章

  1. Scala编程第二课

    函数式编程 函数式编程,结构化编程,OO编程都是编程的方法论. 函数式编程主要思想是把运算过程尽量写成一系列嵌套的函数调用. 特点如下: 1.函数可以像其他数据类型一样使用 可以可以赋值给其他变量,可 ...

  2. Scala 基础(十二):Scala 函数式编程(四)高级(二)参数(类型)推断、闭包(closure)、函数柯里化(curry)、控制抽象

    1  参数(类型)推断 参数推断省去类型信息(在某些情况下[需要有应用场景],参数类型是可以推断出来的,如list=(1,2,3) list.map() map中函数参数类型是可以推断的),同时也可以 ...

  3. 大白话 Scala 控制抽象

    2019-04-14 关键字: Scala.Scala控制抽象.Scala高阶函数 本篇文章系笔者根据当前掌握的知识对 Scala 控制抽象的教材知识总结,不保证文章所述内容的绝对.完全正确性. 在 ...

  4. Scala编程 笔记

    date: 2019-08-07 11:15:00 updated: 2019-11-25 20:00:00 Scala编程 笔记 1. makeRDD 和 parallelize 生成 RDD de ...

  5. Scala编程基础

    Scala与Java的关系... 4 安装Scala. 4 Scala解释器的使用... 4 声明变量... 5 数据类型与操作符... 5 函数调用与apply()函数... 5 if表达式... ...

  6. (升级版)Spark从入门到精通(Scala编程、案例实战、高级特性、Spark内核源码剖析、Hadoop高端)

    本课程主要讲解目前大数据领域最热门.最火爆.最有前景的技术——Spark.在本课程中,会从浅入深,基于大量案例实战,深度剖析和讲解Spark,并且会包含完全从企业真实复杂业务需求中抽取出的案例实战.课 ...

  7. ROS Learning-028 (提高篇-006 A Mobile Base-04) 控制移动平台 --- (Python编程)控制虚拟机器人的移动(不精确的制定目标位置)

    ROS 提高篇 之 A Mobile Base-04 - 控制移动平台 - (Python编程)控制虚拟机器人的移动(不精确的制定目标位置) 我使用的虚拟机软件:VMware Workstation ...

  8. 03.Scala编程实战

    Scala编程实战 1.    课程目标 1.1.  目标:使用Akka实现一个简易版的spark通信框架 2.    项目概述 2.1.   需求 Hivesql----------> sel ...

  9. linux网络编程九:splice函数,高效的零拷贝

    from:http://blog.csdn.net/jasonliuvip/article/details/22600569 linux网络编程九:splice函数,高效的零拷贝 最近在看<Li ...

随机推荐

  1. 对OpenSSL心脏出血漏洞的试验

    1.安装OpenSSL环境 sudo apt-get install openssl sudo pip install pyopenssl(中间会提示ffi.h 没有那个文件或目录,sudo apt- ...

  2. 八、SAP中使用MOVE来赋值

    一.使用MOVE和使用等号的效果是等同的,代码如下: 二.效果如下:

  3. 四、CI框架之通过URL路径访问C中的函数

    一.在C中写一个test001函数 二.在路径http://127.0.0.1/CodeIgniter-3.1.10/index.php/welcome/test001中访问 不忘初心,如果您认为这篇 ...

  4. 服务器上安装解决ole错误

    服务器上安装此插件  提取码:9kiw

  5. 18个python的高效编程技巧

    01 交换变量 >>>a=3 >>>b=6 这个情况如果要交换变量在c++中,肯定需要一个空变量.但是python不需要,只需一行,大家看清楚了 >>& ...

  6. C#高级编程(第9版) 第08章 委托、lambda表达式和事件 笔记

          本章代码分为以下几个主要的示例文件: 1. 简单委托 2. 冒泡排序 3. lambda表达式 4. 事件示例 5. 弱事件     引用方法 委托是寻址方法的.NET版本.在C++中函数 ...

  7. Day 7:TreeSet

    补充上一日:HashCode方法默认返回的是内存地址,String类已经重写了对象的HashCode方法 方法细节:取出数组中的值或字符串的值按照规定计算返回一个值,如果两个字符串内容一致就会返回相同 ...

  8. 高级css效果

    1.图片渐变效果 background linear-gradient(top,rgba(0,0,0,.8),rgba(0,0,0,.8))

  9. Neo4j--图形理论基础

    参考 https://www.w3cschool.cn/neo4j/neo4j_graph_theory_basics.html 节点 圆圈表示的是节点.节点是图表的基本单位. 节点可以用来存储数据, ...

  10. 使用idea断点调试时出现no executable code found at line问题

    问题描述 今天突然碰到了这样的一个问题: 使用断点调试时,断点的地方出现了一个叉号,而不是对勾,这就让我非常无奈了. 调了一天,终于把这个问题解决了,还是要记录一下的. 问题情况如下: 除了这里,de ...