如何在函数式编程中处理可变状态和副作用?

2024年 1月 15日 88.3k 0

函数式编程的不可变和无副作用

首先函数式编程中的比较鲜明的特性就是不可变性和无副作用。

可变 VS 不可变

不可变性简单点说,就是不会改变已经定义的变量

1.变幻莫测的对象状态

在面向对象或者面向过程式的编程中,当遇到一些需要计算累计值的时候,我们通常会定义某个变量,再对变量的赋值不断更新,最后输出变量的最终结果。

假设需要计算班级某门课程平均分,学生分数结构如下:


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

在面向对象或者面向过程式的编程中,我们通常会将计算平均分的方法的实现写成下面这样:

    public Double avgScore(List studentScores) {
        Double sumScore = 0d;
        for (StudentScore studentScore : studentScores) {
            sumScore += studentScore.getScore();
        }
        return sumScore / studentScores.size();
    }

2.函数式与不可变

在函数式编程中,某个变量被定义了之后就不会再改变。同样的计算累计值的场景,在函数式编程中则可以被定义为一连串的函数的链式调用,最后返回最终的结果。

    public Double avgScoreFP(List studentScores) {
        return studentScores.stream().map(StudentScore::getScore)
            .reduce((d1, d2) -> (d1 + d2) / 2).orElse(0d);
    }

这么做的好处就是,代码会更加健壮可靠,对于问题的调查也会更加容易。当我们发现某个计算值有误时,在可变变量的场景中,我们就需要结合实际代码,调查变量所有引用和改动的地方。

当定义的变量都不可变时,问题只会出现在某个较小的函数的计算当中,这部分计算逻辑中。我们只需要关注函数的输入和输出就能调查出具体问题是出在函数调用链的哪个环节上。然后再针对该函数编写相应的单元测试用例,便能保证代码的稳定性。

而且链式的函数调用,每个函数都是较小的计算单元,测试用例的场景也会相对较小,编写单元测试用例时,也会加简单容易。

无副作用 VS 副作用

无副作用是指函数的实现时,不应该对入参做任何更改,并保证对系统是无影响的。这和面向对象编程是有很大差别的。

1.副作用

比如需要更新学生成绩时,面向对象编程,则可能是学生成绩类,会具有一个可以直接设置新成绩的方法来更新学生成绩。

    public void updateScore(StudentScore studentScore, Double newScore) {
        studentScore.setScore(newScore);
    }

这种实现方式,无疑已经对入参 studenScore 造成了影响。如果有更复杂的逻辑,多次更新 studentScore 的 score 属性的值,那么最终,谁也无法预知原先的这个 studentScore 的最终状态是什么样子。

面向对象的最大问题,就是对象状态的不确定性。某个对象经过一连串的方法调用后,很难判断出对象的最终状态,其中如果涉及到缓存,并发等问题,问题的调查则会更加困难。

2.无副作用

在函数式编程中,则完全不同,我们需要定义一个函数,入参为原学生科目信息,和需要更改的成绩最新值,返回值则将是另一个新的学生成绩实例。

  public StudentScore updatedScoreFP(StudentScore studentScore, Double newScore) {
        return new StudentScore(studentScore.id, studentScore.studentId, studentScore.courseId, studentScore.classId, newScore);
    }

而上面的这种写法,我们能够保证原先的 studentScore 是不会被更改的,这个函数无论入参怎么更换,最终的输出都是一个新的 StudentScore 对象。这个函数无论入参怎么变化,无论被调用多少次,对外部系统都是无影响的。

函数式编程所强调的无副作用,是指函数的调用不会对系统、入参造成任何函数功能以外的影响。同一个对象无论调用某个函数多少次,该对象的属性依旧不变。对象新的状态则是通过新的对象体现。这虽然会耗费一些资源,但是能使我们编写的代码更加稳定可靠。

函数式编程的语法支持

对于变量不可变性的实践,java中可以尽量在变量的定义时使用final关键字修饰。对于无副作用的实践,java中并没有专门的语法糖支持,但是JDK1.8之后的 Stream 操作( map, reduce, groupBy 等)以及相关的函数式编程相关的支持都是值得去实践的。

在Scala中对于对象的不可变性,可以通过 case class 来定义纯数据类,保证相关的数据类实例的不可变性,对于一般变量则通过 var 和 val 区分变量是否可变,一般变量尽量使用val关键字修饰以保证其不可变。无副作用的实践上,Scala中对于类对象的操作则可以封装在类的伴生对象中。当然也需要自己在开发过程中具备保证函数无副作用的意识。

上面例子的Scala 2实现如下:


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}
        * */
    }
}

总结

总之函数式编程所强调的的不可变性和无副作用,能够帮助我们编写出更加稳定可靠的代码,构建更加健壮的系统。

  • 以上涉及到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

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论