函数式编程思维让程序变得更好
学习思考
Oct 15, 2023
type
Post
status
Published
date
Oct 15, 2023
slug
functional-thinking
summary
开始拜读 Bob 大叔(Robert C. Martin)最近出的一本新书《Functional Design: Principles, Patterns, and Practices》,想借此机会谈谈自己对函数式编程的思考,以及如何在实际工作中使用函数式思维来成为更好的程序员
tags
思考
函数式编程
category
学习思考
icon
password
Property
Nov 10, 2023 10:33 AM
就像很多其他需要借助技术能力去解决问题的活动一样,编程活动也有很多流派和风格,这些风格有个正式的名称叫编程范式。随着时间的推移和编程技术的发展,越来越多的编程范式被提炼和总结出来,我们今天要探讨的主题函数式编程就是一种活跃于学术界和工业界的编程范式,它起源于学院派理论的 演算(由数学家阿隆佐·邱奇在20世纪30年代首次发表),用几十年的时间悄悄浸染了几乎所有的现代编程语言,包括以面向对象为主的 Java。
当然这篇文章不打算从 演算说起,也不打算介绍那些纯函数式编程的概念和定义,更不会去跟你解释类似“一个单子说白了不过就是自函子范畴上的一个幺半群而已(这句话大家可以背一下,以备调侃时使用😄)”这样的谜语,本文会本着务实的态度,从实际操作出发,从实际工作入手,说明函数式思维是怎么帮助我们解决实际问题,怎么帮助我们成为更好的程序员。
不过开始之前,需要说明本文很多想法和概念都来自书籍《Grokking Simplicity》,推荐大家读读这本比较务实的函数式编程书籍。

尽可能采用计算型结构

提到函数式编程,大家第一印象是这些
  • 函数是第一公民
  • 使用无副作用的函数
  • 不可变数据
  • 不使用全局变量
  • 没有赋值表达式(Bob 大叔新书的观点)
现在我们需要改变对函数式编程的观察视角,从更加务实的角度去审视这些观点,去发现这些观点背后能够给我们的编程活动带来提升的地方,不管我们采用什么编程范式,这都对我们大有裨益。首先要明白这些概念来源于学术理论,我们在实际的软件实现的过程中不能过于死板的去纠结于这些标准,而是要更加灵活的取用适合此时场景的思维方法,比如一定要全部使用无副作用的函数(有时候也叫纯函数)就是过于理想化的理论。
副作用指的是对函数本身以外的范围发起了调用或者造成了影响,下面的程序就不是一个纯函数,它修改了全局变量具有副作用
let alert = false function validateProgrammer(country, age){ if(country !== 'China'){ return } if(age >= 35){ alert = true //修改全局变量就是很明显的副作用 } }
但是仅仅使用纯函数根本行不通,程序的价值很多时候都是需要对外部环境或者状态产生影响来体现,比如我们需要发送短信给客户,如果仅使用纯函数根本做不到。那也不是说纯函数就应该避免使用,相反纯函数有着很多可取之处,所以更多时候我们需要仔细分析需求和程序的组成部分,该使用纯函数的地方一定要积极使用。
《Grokking Simplicity》的作者认为所有的函数式程序都会由三类不同形式的部分组成,分别是 Actions, CalculationsData,我个人觉得 Data 无处不在,是程序运行必须的原材料,所以可以合并到其他两类中,无需作为单独存在的部分来考虑,以此降低认知负载,那么整个程序我们只需要关注两类即可,姑且称之为 Calculation 和 Effect,注意这些类型跟具体的实现语言无关,是程序本身所表现出来的特征,下面我们具体看看这两类程序的区别

Calculation - 计算型

单纯对数据进行计算的程序,对同一个输入只会产生唯一的相同的输出,且不会产生副作用,比如计算两个整数的和、检查一个电子邮件地址是否符合规定等等。
这类程序就是纯函数的绝佳舞台,无论何时调用此程序,也无论调用多少次,都能得到一个恒定的结果,此类代码我们可以放心复用,可以很容易写出更高测试覆盖率的程序,可以作为参数构建更高层次的抽象等。
所以,我们应该在应用中更多的将我们的代码往计算型方向重构来提高我们程序的可重用性、可维护性、可测试性以及可扩展性。

Effect - 影响型

此类程序会收到说运行的环境的影响或者会影响所在的环境,换句话说就是会产生副作用,这类程序比较常见,比如获取当前时间、读写全局变量、发送电子邮件等。
这类程序的结果会受到调用次数和顺序的影响,这导致我们必须审慎对待对此类程序的复用,因为如果不控制好调用的时间和次数,会产生与预期不符的结果。而且此类程序具有传播性,比如 A 函数是影响型的程序,所有直接或者间接调用的程序,都会是影响型的,如果使用很多影响型的程序会让程序中的不稳定因素或者叫脆弱性上升的速率加快。
所以我们应该在程序中避免过多使用此类程序,而且必须仔细审查使用的场景和方式。
上文已经介绍了两种类型代码的利弊,接下来我们来探讨一些能够从影响型程序中抽取计算型代码的方法,为了更加务实,我们从实际需求的实现出发,逐步演示重构和抽取的过程,来展示这些技巧。

需求和原始实现

Taobaobao 是一款在线购物的应用,用户可以通过选购自己喜欢的物品进行线上购物,它的一个关键特点,是购物车在用户购物时始终显示目前选购商品的总费用
notion image
最初实现代码如下(代码出自图书,有修改)
var shopping_cart = []; var shopping_cart_total = 0;   function add_item_to_cart(name, price) { shopping_cart.push ({ name: name, price: price }); calc_cart_total(); update_cart_total_dom(); //更新页面上总价格的 dom }   function calc_cart_total() { for(var i = 0; i < shopping_cart.length; i++) { var item = shopping_cart[i]; shopping_cart_total += item.price ; } } function update_cart_total_dom(){ //show shopping_cart_total to page }

重构

代码完成之后,我们需要测试它,这样才敢放行交付给 QA 和用户,但是这段简单的代码测试起来却很复杂,因为整个代码都是影响型的,需要准备全局变量,也需要准备 UI 测试工具库,才能正常运行我们的测试代码。
那么怎么抽取计算型代码,确保最小化影响型结构,让整个代码变得更好-更容易测试、更容易维护、更容易复用、更內聚?下面介绍的就是很容易操作的步骤
  1. 识别出影响型代码结构 - 这里有三处很明显的 shopping_cart.push()shopping_cart_total += item.priceupdate_cart_total_dom()
  1. 因为影响型代码具有传播性,所以我们需要将它们分离出去,这里以 calc_cart_total 为例说明如何重构使之成为计算型
  1. 一个很简单有用的方法就是显式化参数和返回值,剔除对影响型结构的调用
    1. function calc_cart_total(shopping_cart) { // 显式化声明参数为一个数组 var shopping_cart_total = 0; for(var i = 0; i < shopping_cart.length; i++) { var item = shopping_cart[i]; shopping_cart_total += item.price ; } return shopping_cart_total; // 显式化声明返回值 } // 调用的地方也需要修改 function add_item_to_cart(name, price) { ... var total = calc_cart_total(shopping_cart); ... }
  1. 继续抽取其他部分,直到我们的计算型的业务规则跟影响型结构完全脱离耦合,这样的代码不仅易于维护和测试,还能促使大家认识到业务规则以及领域知识完整的重要性,给代码赋予了更好的封装性
    1. function add_item_to_cart(name, price) { shopping_cart = add_item(shopping_cart, name, price); // 计算型,使用 copy on write shopping_cart_total = calc_cart_total(shopping_cart); // 计算型 update_cart_total_dom() }

总结一下

其实拥有函数式编程思维给程序员带来的帮助绝不止如此,本文只是管中窥豹,略述一二,但是我们如果能够掌握哪怕这一个招式,就能给我们的日常工作带来很大的提升。
更多的计算型结构和尽可能减少影响型结构的好处在总结和上升一个维度来讲就是尽可能让副作用的影响范围被控制在更小的范围,降低程序产生的熵,保持代码结构的简洁。
后面有时间还可以聊一下函数式编程思维中提倡的高层次抽象给我们带来的好处,其实这个更加有用,不仅是在写代码的时候,它还能帮助我们成为更加优秀的架构师。
 
  • 思考
  • 函数式编程
  • 写作课习作汇总
    软件研发团队的信息管理