如何在函數(shù)式編程中處理可變狀態(tài)和副作用?
函數(shù)式編程的不可變和無副作用
首先函數(shù)式編程中的比較鮮明的特性就是不可變性和無副作用。
可變 VS 不可變
不可變性簡單點(diǎn)說,就是不會改變已經(jīng)定義的變量
1.變幻莫測的對象狀態(tài)
在面向?qū)ο蠡蛘呙嫦蜻^程式的編程中,當(dāng)遇到一些需要計算累計值的時候,我們通常會定義某個變量,再對變量的賦值不斷更新,最后輸出變量的最終結(jié)果。
假設(shè)需要計算班級某門課程平均分,學(xué)生分?jǐn)?shù)結(jié)構(gòu)如下:
class StudentScore {
public String id;
public String studentId;
public String courseId;
public String classId;
public Double score;
public StudentScore(String id, String studentId, String courseId, String classId, Double score) {
this.id = id;
this.studentId = studentId;
this.courseId = courseId;
this.classId = classId;
this.score = score;
}
public Double getScore() {
return score;
}
}
在面向?qū)ο蠡蛘呙嫦蜻^程式的編程中,我們通常會將計算平均分的方法的實現(xiàn)寫成下面這樣:
public Double avgScore(List<StudentScore> studentScores) {
Double sumScore = 0d;
for (StudentScore studentScore : studentScores) {
sumScore += studentScore.getScore();
}
return sumScore / studentScores.size();
}
2.函數(shù)式與不可變
在函數(shù)式編程中,某個變量被定義了之后就不會再改變。同樣的計算累計值的場景,在函數(shù)式編程中則可以被定義為一連串的函數(shù)的鏈?zhǔn)秸{(diào)用,最后返回最終的結(jié)果。
public Double avgScoreFP(List<StudentScore> studentScores) {
return studentScores.stream().map(StudentScore::getScore)
.reduce((d1, d2) -> (d1 + d2) / 2).orElse(0d);
}
這么做的好處就是,代碼會更加健壯可靠,對于問題的調(diào)查也會更加容易。當(dāng)我們發(fā)現(xiàn)某個計算值有誤時,在可變變量的場景中,我們就需要結(jié)合實際代碼,調(diào)查變量所有引用和改動的地方。
當(dāng)定義的變量都不可變時,問題只會出現(xiàn)在某個較小的函數(shù)的計算當(dāng)中,這部分計算邏輯中。我們只需要關(guān)注函數(shù)的輸入和輸出就能調(diào)查出具體問題是出在函數(shù)調(diào)用鏈的哪個環(huán)節(jié)上。然后再針對該函數(shù)編寫相應(yīng)的單元測試用例,便能保證代碼的穩(wěn)定性。
而且鏈?zhǔn)降暮瘮?shù)調(diào)用,每個函數(shù)都是較小的計算單元,測試用例的場景也會相對較小,編寫單元測試用例時,也會加簡單容易。
無副作用 VS 副作用
無副作用是指函數(shù)的實現(xiàn)時,不應(yīng)該對入?yún)⒆鋈魏胃?,并保證對系統(tǒng)是無影響的。這和面向?qū)ο缶幊淌怯泻艽蟛顒e的。
1.副作用
比如需要更新學(xué)生成績時,面向?qū)ο缶幊蹋瑒t可能是學(xué)生成績類,會具有一個可以直接設(shè)置新成績的方法來更新學(xué)生成績。
public void updateScore(StudentScore studentScore, Double newScore) {
studentScore.setScore(newScore);
}
這種實現(xiàn)方式,無疑已經(jīng)對入?yún)?nbsp;studenScore 造成了影響。如果有更復(fù)雜的邏輯,多次更新 studentScore 的 score 屬性的值,那么最終,誰也無法預(yù)知原先的這個 studentScore 的最終狀態(tài)是什么樣子。
面向?qū)ο蟮淖畲髥栴},就是對象狀態(tài)的不確定性。某個對象經(jīng)過一連串的方法調(diào)用后,很難判斷出對象的最終狀態(tài),其中如果涉及到緩存,并發(fā)等問題,問題的調(diào)查則會更加困難。
2.無副作用
在函數(shù)式編程中,則完全不同,我們需要定義一個函數(shù),入?yún)樵瓕W(xué)生科目信息,和需要更改的成績最新值,返回值則將是另一個新的學(xué)生成績實例。
public StudentScore updatedScoreFP(StudentScore studentScore, Double newScore) {
return new StudentScore(studentScore.id, studentScore.studentId, studentScore.courseId, studentScore.classId, newScore);
}
而上面的這種寫法,我們能夠保證原先的 studentScore 是不會被更改的,這個函數(shù)無論入?yún)⒃趺锤鼡Q,最終的輸出都是一個新的 StudentScore 對象。這個函數(shù)無論入?yún)⒃趺醋兓?,無論被調(diào)用多少次,對外部系統(tǒng)都是無影響的。
函數(shù)式編程所強(qiáng)調(diào)的無副作用,是指函數(shù)的調(diào)用不會對系統(tǒng)、入?yún)⒃斐扇魏魏瘮?shù)功能以外的影響。同一個對象無論調(diào)用某個函數(shù)多少次,該對象的屬性依舊不變。對象新的狀態(tài)則是通過新的對象體現(xiàn)。這雖然會耗費(fèi)一些資源,但是能使我們編寫的代碼更加穩(wěn)定可靠。
函數(shù)式編程的語法支持
對于變量不可變性的實踐,java中可以盡量在變量的定義時使用final關(guān)鍵字修飾。對于無副作用的實踐,java中并沒有專門的語法糖支持,但是JDK1.8之后的 Stream 操作( map, reduce, groupBy 等)以及相關(guān)的函數(shù)式編程相關(guān)的支持都是值得去實踐的。
在Scala中對于對象的不可變性,可以通過 case class 來定義純數(shù)據(jù)類,保證相關(guān)的數(shù)據(jù)類實例的不可變性,對于一般變量則通過 var 和 val 區(qū)分變量是否可變,一般變量盡量使用val關(guān)鍵字修飾以保證其不可變。無副作用的實踐上,Scala中對于類對象的操作則可以封裝在類的伴生對象中。當(dāng)然也需要自己在開發(fā)過程中具備保證函數(shù)無副作用的意識。
上面例子的Scala 2實現(xiàn)如下:
package demo.basic
case class StudentScore(id: String,
studentId: String,
courseId: String,
classId: String,
score: Double) {
override def toString: String =
s"StudentScore:{id: ${id}, studentId:${studentId}, courseId:${courseId}, score: ${score}}"
}
object StudentScore {
def avgScore(studentScores: Array[StudentScore]): Double = {
studentScores.map[Double](s => s.score).reduce((s1, s2) => (s1 + s2) / 2)
}
def updateScore(studentScore: StudentScore, newScore: Double): StudentScore = {
StudentScore(studentScore.id, studentScore.studentId, studentScore.courseId, studentScore.classId, newScore)
}
}
object FunctionalProgramingDemo {
def main(args: Array[String]): Unit = {
val s1 = StudentScore("id-0001", "student-0001", "course-0001", "class-0001", 83.5)
val s2 = StudentScore("id-0002", "student-0002", "course-0001", "class-0001", 82.0)
val s3 = StudentScore("id-0002", "student-0003", "course-0001", "class-0001", 81.0)
val scores = Array(s1, s2, s3)
val avgScore = StudentScore.avgScore(scores)
println(avgScore)
val s3New = StudentScore.updateScore(s3, 79.5)
println(s3)
println(s3New)
/*
81.875
StudentScore:{id: id-0002, studentId:student-0003, courseId:course-0001, score: 81.0}
StudentScore:{id: id-0002, studentId:student-0003, courseId:course-0001, score: 79.5}
* */
}
}
總結(jié)
總之函數(shù)式編程所強(qiáng)調(diào)的的不可變性和無副作用,能夠幫助我們編寫出更加穩(wěn)定可靠的代碼,構(gòu)建更加健壯的系統(tǒng)。
- 以上涉及到Java部分的代碼的 GitHub 鏈接:https://github.com/stevenzearo/ichat/blob/master/demo/java-demo/src/main/java/basic/FunctionalProgramingDemo.java
- 涉及到Scala部分的代碼的 GitHub 鏈接:https://github.com/stevenzearo/scala-gradle/blob/master/src/main/scala/demo/basic/FunctionalProgramingDemo.scala