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