摘要:使用Scala语言为例,展示函数式编程消除重复无聊的foreach代码。

难度:中级

概述###

大多数开发者在开发生涯里,会面对大量业务代码。而这些业务代码中,会发现有大量重复无聊的 foreach 循环,有时是为了获取对象的一个关键字段的值,有时是为了设置对象的某些字段的值,有时是为了转换得到另外一个对象,有时是为了增加若干新的字段。主要有如下情况:

  • map origin object to new object in order to get new list or new map ; 将一个对象映射为另一个对象,得到一个新的列表;
  • filter some objects to get new list according to condition function; 根据某个条件函数,过滤出所需要的对象列表;
  • if-add, if-remove, if-set ; 在满足某种条件的情况下, 设置对象的某些字段的值, 为对象动态增加若干字段、从列表中直接移除对象;
  • 聚合操作。在满足某种条件的情况下,抽取所指定对象的某些字段的值并进行聚合操作。聚合操作比如求和、最大值、合并等。

注意到 filter 和 if-remove 的区别。 一般来说, filter 会返回一个全新的不可变列表,拥有并发安全性,会有若干空间开销,只要列表不是特别大,都可以选用; 而 if-remove 则会直接从原列表中移除元素,导致列表可变, 不拥有并发安全,节省若干空间开销,适合于列表很大的情况。

实际上,这些foreach 代码完全可以使用函数式编程来消除重复一遍遍地写 foreach , 而专注于遍历里需要做的操作和业务逻辑。

代码示例###

以下显示了Scala函数式编程如何消除业务层的foreach代码。

object Sex extends Enumeration {
val Female = Value("Female")
val Male = Value("Male")
val Double = Value("Double")
} class Person(var name:String, var age:Int, var ables:List[String], val sex:Sex.Value) { def setAge(age:Int):Unit = {
this.age = age
} def empty():String = { return "" } def getValue(fieldName:String):Any = {
fieldName match {
case "name" => name
case "age" => age
case "ables" => ables
case "sex" => sex
case _ => empty
}
} override def toString = {
s"${this.name} is ${this.sex} sex , ${this.age} years old, able to do : " + this.ables.mkString("'",",", "'")
}
} object PersonsVisitor { /**
* Map Processing Pattern
* Fetch part fields from object list.
*/
def getField(persons:List[Person], fieldName:String):List[Any] = {
persons.map(p => p.getValue(fieldName))
} /**
* Filter Processing Pattern
* Get some objects satisfy condition function from object list
*/
def filter(persons:List[Person], accept:(Person => Boolean)): List[Person] = {
persons.filter(accept)
} /**
* Aggregation Operation Pattern, eg. concat, sum
*/
def aggregate[T](persons:List[Person], op: (Person => T), aggre: (List[T] => T)): T = {
aggre(persons.map(op))
} /**
* If-Set Operation Pattern
*/
def ifSet(persons:List[Person], accept:(Person=>Boolean), setFunc: (Person=>Unit)): List[Person] = {
persons.foreach { p => if (accept(p)) { setFunc(p) } }
persons
} /**
* If-Remove Operation Pattern
*/
def ifRemove(persons:List[Person], accept:(Person=>Boolean)):Iterator[Person] = {
persons.iterator.filter(p => ! accept(p) )
} def buildPersons():List[Person] = {
List( new Person("Lier", 20, List("Study", "Explore","Combination"), Sex.Male),
new Person("lover", 16, List("Love","Chat","Combination"), Sex.Female),
new Person("Tender", 18, List("Care", "Combination"), Sex.Double)) }
} object NoRepeatForeach extends App { launch() def launch():Unit = {
val persons = PersonsVisitor.buildPersons()
println(PersonsVisitor.getField(persons, "name"))
println(PersonsVisitor.getField(persons, "ables"))
println(PersonsVisitor.getField(persons, "sex"))
println(PersonsVisitor.getField(persons, "none")) PersonsVisitor.filter(persons, p => p.ables.contains("Care")).foreach { println _ } println("All ables: " + PersonsVisitor.aggregate(persons, p=>p.ables.mkString(","), (ablelist:List[String]) => ablelist.toSet.mkString(",")))
println("Total age: " + PersonsVisitor.aggregate(persons, p=>p.age, (agelist:List[Int]) => agelist.sum))
println("If-Set:" + PersonsVisitor.ifSet(persons, p=> p.age >= 18, p=> p.setAge(p.age+1) ))
println("If-Remove: " + PersonsVisitor.ifRemove(persons, p=> p.age >= 18).toList)
} }

输出如下:

List(Lier, lover, Tender)
List(List(Study, Explore, Combination), List(Love, Chat, Combination), List(Care, Combination))
List(Male, Female, Double)
List(, , )
Tender is Double sex , 18 years old, able to do : 'Care,Combination'
All ables: Study,Explore,Combination,Love,Chat,Combination,Care,Combination
Total age: 54
If-Set:List(Lier is Male sex , 21 years old, able to do : 'Study,Explore,Combination', lover is Female sex , 16 years old, able to do : 'Love,Chat,Combination', Tender is Double sex , 19 years old, able to do : 'Care,Combination')
If-Remove: List(lover is Female sex , 16 years old, able to do : 'Love,Chat,Combination')

代码讲解###

  • object Sex extends Enumeration , 定义了枚举 Sex: 枚举类型为 Sex.Value ;
  • class Person(var name:String, var age:Int, var ables:List[String], val sex:Sex.Value) 将类定义与主构造器结合起来。 使用 var ables:String 可以使得Scala自动生成 ables() 和 ables_$eq() 方法, 从而可以用 p.ables 来引用(实际上引用的是 ables() 方法); 如果不写 var 是不会自动生成相应方法的,也就不能用 p.ables 来引用了。
  • s"${this.name} is ${this.sex} sex , ${this.age} years old, able to do : " + this.ables.mkString("'",",", "'") 显示了Scala 中字符串插值的用法;

函数式编程####

核心都在对象 PersonsVisitor 里。

  • getField 使用 map 函数动态可配置地提取对象列表的指定字段的值列表;
  • filter 使用 filter 函数根据指定条件函数 accept 过滤出所需要的对象列表;
  • aggregate 则展示了一类常用操作:根据指定条件函数 accept 过滤出所需要的对象列表的某些值,然后对这些值做聚合操作,得到一个最终值;
  • ifSet 展示了一类常用操作: 根据指定条件函数 accept 过滤出所需要的对象并设置一些字段的值,得到改变后的对象列表;
  • ifRemove 展示了一类相对少见的操作: 根据指定条件函数 accept 直接从原列表中移除指定元素, 通常是有点对空间开销过于敏感了。注意到,这里使用了迭代器作为中间层,通过迭代器指向不满足条件的元素并返回其列表,不可变地实现获得“从原列表中移除指定元素后的原列表”。 实际上原列表并没有变化,只是通过迭代器实现了移除元素的视图。有点类似SQL 的 View 概念。

循环消失了么####

循环消失了么? No ! 是,也不是。 循环从业务代码中消失了。 但它并不是真正彻底底从代码里消失了。 循环被隐藏在抽象层里。 这样有什么益处呢? 抽象层的最重要作用就是“分离关注点”。 ORM 抽象层分离了“数据访问与对象之间的转化”的关注点, Storm框架抽象层分离了“分布式计算模型、拓扑以及节点消息传递”的关注点,使得应用只关注业务层的逻辑。

函数式编程也是一样,分离了“批量、流式处理列表数据的基础流程逻辑” 的关注点,使得业务层只需要专注于元素处理和获取结果。 你不必一次次写 foreach XXX , 而是只要编写定制的业务逻辑方法即可。

更通用的版本###

可以使用泛型将 PersonsVisitor 写得更通用一些。

trait FieldValue {
def getValue(fieldName:String):Any = {}
} object Visitor { /**
* Map Processing Pattern
* Fetch part fields from object list.
*/
def getField[T <: FieldValue](objs:List[T], fieldName:String):List[Any] = {
objs.map(p => p.getValue(fieldName))
} /**
* Filter Processing Pattern
* Get some objects satisfy condition function from object list
*/
def filter[T](objs:List[T], accept:(T => Boolean)): List[T] = {
objs.filter(accept)
} /**
* Aggregation Operation Pattern, eg. concat, sum
*/
def aggregate[R,T](objs:List[R], op: (R => T), aggre: (List[T] => Any)): Any = {
aggre(objs.map(op))
} /**
* If-Set Operation Pattern
*/
def ifSet[T](objs:List[T], accept:(T=>Boolean), setFunc: (T=>Unit)): List[T] = {
objs.foreach { p => if (accept(p)) { setFunc(p) } }
objs
} /**
* If-Remove Operation Pattern
*/
def ifRemove[T](objs:List[T], accept:(T=>Boolean)):Iterator[T] = {
objs.iterator.filter(p => ! accept(p) )
} def buildPersons():List[Person] = {
List( new Person("Lier", 20, List("Study", "Explore","Combination"), Sex.Male),
new Person("lover", 16, List("Love","Chat","Combination"), Sex.Female),
new Person("Tender", 18, List("Care", "Combination"), Sex.Double)) }
} object NoRepeatForeachGeneral extends App { launch() def launch():Unit = {
val persons = Visitor.buildPersons()
println(Visitor.getField(persons, "name"))
println(Visitor.getField(persons, "ables"))
println(Visitor.getField(persons, "sex"))
println(Visitor.getField(persons, "none")) Visitor.filter(persons, (p:Person) => p.ables.contains("Care")).foreach { println _ } println("All ables: " + Visitor.aggregate(persons, (p:Person)=>p.ables.mkString(","), (ablelist:List[String]) => ablelist.toSet.mkString(",")))
println("Total age: " + Visitor.aggregate(persons, (p:Person)=>p.age, (agelist:List[Int]) => agelist.sum))
println("If-Set:" + Visitor.ifSet(persons, (p:Person)=> p.age >= 18, (p:Person)=> p.setAge(p.age+1) ))
println("If-Remove: " + Visitor.ifRemove(persons, (p:Person)=> p.age >= 18).toList)
} }

代码讲解二###

  • 为了将 getField 泛型化, 需要保证类型 T 具有 getValue 方法,这通常通过定义接口来实现约束关系。 定义一个含有 getValue 方法的 trait FieldValue , 然后在泛型声明中声明 T <: FieldValue, 表明 T 是 FieldValue 的子类型,这样,Scala 可以推断出 T 类型可以调用 getValue 方法了。
  • 注意到,当 Visitor 通过泛型更加通用化后,客户端代码会有一些负担。 原来只要写成 p => p.age >= 18 , 现在需要写成 (p:Person) => p.age >= 18 。 必须声明参数类型,否则 Scala 无法判断 p 是否有方法 age()。

柯里化改造###

为了让客户端代码写得更舒服些,应该尽量让Scala自行推导出 p 的类型拥有 age() 方法。一开始是想用泛型, 定义 class Visitor[T] 或 trait Visitor[T]; 可是 Scala无法将函数里的函数参数 (比如 accept:(T=>Boolean)) 的类型 T 推导成带入的类型: val visitor = new Visitor[Person]. 在文章 Scala类型推导 谈到柯里化可以做到这一点,立即尝试了,是可行的。柯里化实际上就是将一次多参数调用过程分解成多个单参数调用步骤。见如下代码。能否用泛型来实现,作为一个待解之谜。 

object Visitor {

  /**
* Map Processing Pattern
* Fetch part fields from object list.
*/
def getField[T <: FieldValue](objs:List[T], fieldName:String):List[Any] = {
objs.map(p => p.getValue(fieldName))
} /**
* Filter Processing Pattern
* Get some objects satisfy condition function from object list
*/
def filter[T](objs:List[T])(accept:(T => Boolean)): List[T] = {
objs.filter(accept)
} /**
* Aggregation Operation Pattern, eg. concat, sum
*/
def aggregate[R,T](objs:List[R])(op: (R => T))(aggre: (List[T] => Any)): Any = {
aggre(objs.map(op))
} /**
* If-Set Operation Pattern
*/
def ifSet[T](objs:List[T])(accept:(T=>Boolean))(setFunc: (T=>Unit)): List[T] = {
objs.foreach { p => if (accept(p)) { setFunc(p) } }
objs
} /**
* If-Remove Operation Pattern
*/
def ifRemove[T](objs:List[T])(accept:(T=>Boolean)):Iterator[T] = {
objs.iterator.filter(p => ! accept(p) )
} def buildPersons():List[Person] = {
List( new Person("Lier", 20, List("Study", "Explore","Combination"), Sex.Male),
new Person("lover", 16, List("Love","Chat","Combination"), Sex.Female),
new Person("Tender", 18, List("Care", "Combination"), Sex.Double)) }
} object NoRepeatForeachSoft extends App { launch() def launch():Unit = {
val persons = Visitor.buildPersons()
println(Visitor.getField(persons, "name"))
println(Visitor.getField(persons, "ables"))
println(Visitor.getField(persons, "sex"))
println(Visitor.getField(persons, "none")) Visitor.filter(persons)(p => p.ables.contains("Care")).foreach { println _ } println("All ables: " + Visitor.aggregate(persons)(p=>p.ables.mkString(","))(ablelist => ablelist.toSet.mkString(",")))
println("Total age: " + Visitor.aggregate(persons)(p=>p.age)(agelist => agelist.sum))
println("If-Set:" + Visitor.ifSet(persons)(p=> p.age >= 18)(p=> p.setAge(p.age+1)))
println("If-Remove: " + Visitor.ifRemove(persons)(p=> p.age >= 18).toList)
} }

代码讲解三###

举一个简单的函数来说。filter初始定义是两个参数: def filter[T](objs:List[T], accept:(T => Boolean)): List[T],传入一个T类型的对象列表和一个以T类型对象为参数的条件函数。柯里化之后:def filter[T](objs:List[T])(accept:(T => Boolean)): List[T],参数未变,编写形式发生了变化,调用方式也发生了变化: Visitor.filter(persons)(p => p.ables.contains("Care")) . 类似于一个二元函数求值,可以一次性将参数全部代入,也可以一次代入一个参数求值。 

注意到,客户端代码中传入的函数再也不需要指明参数类型了。Scala可以根据调用者对象自动推导出传入函数的参数类型。

小结###

可以看到,使用函数式编程,将通用流程处理(遍历-条件-执行操作)与定制业务逻辑(业务对象列表、业务操作)清晰地分离开,各司其责。业务代码再也不用充斥一条条单调无味的foreach语句了。

有人说,函数式编程有内存和性能开销,高阶函数的可理解性和可维护性相对较低,应用于大型工程可能有潜在风险。对此,我的观点是:语言和技术终会进化,今日所忧虑的问题在明日会变成家常便饭一样接受。勇往直前吧。

使用函数式编程消除重复无聊的foreach代码(Scala示例)的更多相关文章

  1. 编程中的链式调用:Scala示例

    编程中的链式调用与Linux Shell 中的管道类似.Linux Shell 中的管道 ,会将管道连接的上一个程序的结果, 传递给管道连接的下一个程序作为参数进行处理,依次串联起N个实用程序形成流水 ...

  2. Scala入门系列(十):函数式编程之集合操作

    1. Scala的集合体系结构 Scala中的集合体系主要包括(结构跟Java相似): Iterable(所有集合trait的根trait) Seq(Range.ArrayBuffer.List等) ...

  3. Scala学习教程笔记三之函数式编程、集合操作、模式匹配、类型参数、隐式转换、Actor、

    1:Scala和Java的对比: 1.1:Scala中的函数是Java中完全没有的概念.因为Java是完全面向对象的编程语言,没有任何面向过程编程语言的特性,因此Java中的一等公民是类和对象,而且只 ...

  4. Java8函数式编程探秘

    引子 将行为作为数据传递 怎样在一行代码里同时计算一个列表的和.最大值.最小值.平均值.元素个数.奇偶分组.指数.排序呢? 答案是思维反转!将行为作为数据传递. 文艺青年的代码如下所示: public ...

  5. 9、scala函数式编程-集合操作

    一.集合操作1 1.Scala的集合体系结构 // Scala中的集合体系主要包括:Iterable.Seq.Set.Map.其中Iterable是所有集合trait的根trai.这个结构与Java的 ...

  6. 如何编写高质量的 JS 函数(4) --函数式编程[实战篇]

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/ZoXYbjuezOWgNyJKmSQmTw作者:杨昆 [编写高质量函数系列],往期精彩内容: ...

  7. scala 函数式编程之集合操作

    Scala的集合体系结构 // Scala中的集合体系主要包括:Iterable.Seq.Set.Map.其中Iterable是所有集合trait的根trai.这个结构与Java的集合体系非常相似. ...

  8. Scala:函数式编程之下划线underscore

    http://blog.csdn.net/pipisorry/article/details/52913548 python参考[python函数式编程:apply, map, lambda和偏函数] ...

  9. Python3基础(3)集合、文件操作、字符转编码、函数、全局/局部变量、递归、函数式编程、高阶函数

    ---------------个人学习笔记--------------- ----------------本文作者吴疆-------------- ------点击此处链接至博客园原文------ 1 ...

随机推荐

  1. java JDBC (四)

    package cn.sasa.demo4; import java.sql.Connection; import java.sql.PreparedStatement; import java.sq ...

  2. 洛谷P3247 最小公倍数 [HNOI2016] 分块+并查集

    正解:分块+并查集 解题报告: 传送门! 真的好神仙昂QAQ,,,完全想不出来,,,还是太菜了QAQ 首先还是要说下,这题可以用K-D Tree乱搞过去(数据结构是个好东西昂,,,要多学学QAQ),但 ...

  3. 如何在win+r 或者是win10的应用搜索输入subl就能打开sublime

    这虽然不是什么技术贴,我实在不想开启sublime还要动鼠标,或者输入subl长长的全称,这里有两种做法: 第一种 在环境变量添加sublime安装目录的变量,一般sublime的安装目录会有subl ...

  4. JavaScript学习(五)

  5. 使用jframe编写一个base64加密解密工具

    该工具可以使用exe4j来打包成exe工具(如何打包自己百度) 先上截图功能 运行main方法后,会弹出如下窗口 输入密文 然后点击解密,在点格式化 代码分享 package tools;import ...

  6. C++的string

    string中find()返回值是字母在母串中的位置(下标记录),如果没有找到,返回npos. string的substr(pos=0, count=npos)返回字符串[pos, pos+count ...

  7. git bash 报错bash: *: command not found

    默认安装的git bash某些功能是没有的,比如zip,在git bash下执行zip和unzip命令时会报错命令找不到,但值得庆幸的是,我们可以安装我们需要的命令,以下以zip命令为例,步骤如下: ...

  8. [LeetCode] 441. Arranging Coins_Easy tag: Math

    You have a total of n coins that you want to form in a staircase shape, where every k-th row must ha ...

  9. \r\n 回车换行浅析

    \r \ 10 x0a return \n \ x0d newline Unix系统里,每行结尾只有“<换行>”,即“\n”: Windows系统里面,每行结尾是“<回车>&l ...

  10. vue中使用hotcss--stylus

    页面中一直闪动这个. 后面改成scss后还是这样.还不知道原因