摘要:使用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語句了。
有人說,函數式編程有內存和性能開銷,高階函數的可理解性和可維護性相對較低,應用於大型工程可能有潛在風險。對此,我的觀點是:語言和技術終會進化,今日所憂慮的問題在明日會變成家常便飯一樣接受。勇往直前吧。