自从高中开始学编程已经使用过好几门(PL)编程语言了,目前主流最多使用的编程语言 C/C++ 、Java 、JavaScript 这些都属于结构式和命令式编程语言。但在 Java 在 8 版本发布之后引入了 Lambda 表达式方式对函数式编程的支持,从现代的 Java 设计角度上来说并不是一个纯粹的结构和命令式编程语言了,个人将其归属为一个综合性很强的多编程范式的语言。在过去一年的重新学习了基于 ES6 版本的 JavaScript 之后,让我对函数式编程语言更为感兴趣了。在此之前使用过 Go 语言的实现过一些类似于函数式编程的代码,但 Go 在语言设计的时候并没有添加三目运算符和 Lambda 表达式,使其在编程体验上逊色于 JavaScript 和 Java8 这类原生支持三目运算符和 Lambda 表达式的函数式语言,目前我已经从完全放弃了 Go 语言传统命令式进行编程了,除了几个极少历史遗留开源项目正在使用它进行维护之外。如果读者正在考虑深入学习一门 PL 编程语言的话,我建议学习并且使用现代 Java21 之后的版本,现代 Java 已经添加很多新特性,这篇文章我会介绍一下函数式编程的优势。


面向过程

程序员日常开发中使用的最多编程方式是面向过程和面向对象,面向对象 OOP 是从 C 语言的面向过程演变发展设计出来的;面向过程取决于程序员如何设计一个好的数据结构和用编写好的函数去操作数据完成逻辑计算,在面向过程编程代码逻辑过程中,函数是主要的组织单元,程序的执行是一系列的函数调用,使用结构体来组织数据,但是能操作这些结构体仍然是通过函数来进行的。

编程范式最典型的例子,生活中一件事情抽象到计算机程序如何做?例如一个抽象的问题:如何把一头大象放到冰箱里面该如何做?换到人类的思维第一反应是怎么可能会有冰箱能装得下一头大象?在计算机里你只要想将具体逻辑告诉它,它能可以按照你逻辑去做某件事情,它很笨不会考虑这么多其他因素,不能去考虑大象是不是比冰箱大,除非在装填函数中编写了一些逻辑来检查是否合规。

下面就是使用 C 语言就编写一个 3 个函数把大象结构放到冰箱结构里,每个步骤对应着一个操作函数,面向过程的窜行代码:

#include <stdio.h>
#include <stdbool.h>

// 定义大象的结构体
struct Elephant {
    char name[20];
    int weight;
};

// 定义冰箱的结构体
struct Refrigerator {
    bool isOpen;
    bool hasElephant;
};

// 函数原型
void openRefrigerator(struct Refrigerator *fridge);
void closeRefrigerator(struct Refrigerator *fridge);
void placeElephantInRefrigerator(struct Elephant *elephant, struct Refrigerator *fridge);

int main() {
    // 创建大象对象
    struct Elephant elephant = {"大象", 5000};
    
    // 创建冰箱对象
    struct Refrigerator refrigerator = {false, false};
    
    printf("将大象放到冰箱里面:\n");
    
    // 打开冰箱
    openRefrigerator(&refrigerator);
    
    // 将大象放到冰箱里
    placeElephantInRefrigerator(&elephant, &refrigerator);
    
    // 关上冰箱
    closeRefrigerator(&refrigerator);
    
    return 0;
}

// 打开冰箱的函数
void openRefrigerator(struct Refrigerator *fridge) {
    printf("打开冰箱门\n");
    fridge->isOpen = true;
}

// 关闭冰箱的函数
void closeRefrigerator(struct Refrigerator *fridge) {
    printf("关闭冰箱门\n");
    fridge->isOpen = false;
}

// 将大象放到冰箱里的函数
void placeElephantInRefrigerator(struct Elephant *elephant, struct Refrigerator *fridge) {
    if (fridge->isOpen) {
        printf("将 %s 放进冰箱\n", elephant->name);
        fridge->hasElephant = true;
    } else {
        printf("无法将大象放进冰箱,因为冰箱门没有打开\n");
    }
}

面向过程重点就是数据和算法的组织,采用顺序执行函数的方式,通常会存在一些全局结构体变量,这些变量会在函数之间通过参数传递;面向过程编程解决问题是:如何设计解决问题的过程? 这里的过程是指的是函数和函数之间的关系,将问题分解为一个一个小函数,而问题所需要的元数据信息则被抽象为了变量,变量可以传入到函数中执行改变其状态,一步一步顺序执行调用这些过程函数就可以解决问题。


面向对象

典型的面向过程的编程语言是 C 语言,在很长一段历史时间内程序员唯一选择就是使用 C 语言进行面向过程进行程序设计,在复杂业务场景下没有更好的其他 PL 替代选择;只能把编程中所需要的数据变量简单的抽象,在不断的代码项目迭代过程中可能会代码的重用性和可维护性较差,编写的函数和结构体之间没有任何的关联,只能通过显示的函数实参进行调用,使得函数和具体结构体类型之间没有明确绑定关系,在需求不断变化过程中扩展能力差,不利于后期对代码结构进行扩展。直到后面 Smalltalk 、C++ 、Objective-C 、Java 等 Object-Oriented Programming 语言的出现给计算机编程领域加快发展,使得程序员可以用其他编程语言选择了。

在面向对象编程中提倡的是程序员将有状态的变量和能修改变量状态的函数封装成一个单一对象,其对象被作为基本的程序操作单元,把变量和函数封装一个单独对象,通过调用对象上面的函数这里称之为对象的方法(method),通过方法来操作对象状态数据的变更。这些抽象的对象又可以彼此之间具有相互依赖性,对象可以包含另外一个对象,对象方法可以接受另外一个对象作为参数或者返回另一个对象,让程序员编程只需要关注对象之间关系,通过操作对象的实例来解决程序设计问题。

正如上图一辆汽车可以被抽象为一个 Class 名为 Car 基础类型,汽车的属性信息是可变的可以抽象为对应的对象属性字段,而汽车所应用基础功能可以抽象为功能方法。如果需要编程的过程中需要使用汽车的时候,就可以使用 Car 类型创建出来一辆自定义的汽车,每辆汽车可以具有不同的属性值,但都拥有相同的方法和功能。

面向对象编程思想本质是现实世界中的事物进行抽象,通过抽象的表达来模拟现实世界中物理关系,此种编程方式更符合人类的直觉,编程过程中关注的是对象与对象之间的关系。对象是通过一个物抽象模版 Class 创建出来的实例,通过实例进行编程。在编程过程中程序员都是提倡如果一段代码被重复的使用,或者类似的代码不止出现于一次,可以考虑将其共同的特性抽象出来。这样对应了面向对象编程的 3 大特性:继承 、封装 、多态,笔者认为面向对象编程最终编程模式是面向接口 interface 编程,程序员更应该更关注是对象之间共同特性就其抽象出来,而不需要关注实现对象的数据结构和算法功能实现的细节,抽象的一个对象包含了数据和操作数据的函数。


函数编程

在 OOP 编程风格中一个 Class 在程序运行中被实例化成为一个对象变量,变量对象中存储运行时所需要的数据信息,在运行时通过对象进行修改其内部属性的值,我们可以改变对象的状态,从而影响程序的行为。这在单线程编程环境很好的设计,但是在多线程环境未必很好设计,当程序在并行执行的时候会涉及到多线程的数据竞争问题,当多个线程同时访问和修改共享的对象状态时,可能会出现不确定的行为和错误的结果。

在函数编程中提倡像数学中函数一样求值,强调函数的纯粹性、不可变性和无副作用性,避免修改可变状态和使用可变的数据。这个怎么理解呢?大家在编程中可能使用过类似于 int add(int x,int y){ ... } 这样的函数,该函数对传入的参数进行求值计算。这个 add(x,y) 函数体里面的逻辑就不会对外部的数据进行修改,而是进行某些数学计算得到另外一个结果,纯函数是指函数的输出只依赖于输入不会对外部状态造成影响,也不会产生副作用,即给定相同的输入永远得到相同的输出。下面是使用 C 语言编写一个纯函数例子:

#include <stdio.h>

// 定义一个纯函数 add,接受两个参数并返回它们的和
int add(int x, int y) {
    return x + y;
}

int main() {
    // 1. 使用 add 函数来执行整数相加的操作
    // 2. 调用 add 函数并将返回值赋给 result
    int result = add(5, 3); 

    // 3. 输出结果,输出结果:8
    printf("%d\n", result); 

    return 0;
}

在这段代码中声明一个 add(x,y) 纯函数,它接受两个参数并返回它们的和,不需要依赖外部的条件变量,而且不会改变任何外部状态。

与传统指令式编程不同,函数编程是围绕着函数进行的,在传统的指令式编程范式中,程序通常被组织为一系列的语句和命令,这些命令会改变程序的状态和数据。而在函数编程中,程序的主要执行单元是函数,它们接受输入并产生输出,而不会改变外部状态或数据。函数之间可以被组合、嵌套和传递,从而实现复杂的逻辑和操作。

下面这段 C 语言代码就是使用函数式的方式抽象中缀表达式计算过程函数式编程,中缀表达式是数学和计算机科学中常见的表达式形式,其中运算符位于操作数之间。当中缀表达式被抽象为函数的时,每个函数就对应着每个运算符和具体计算步骤,总体整合就会形成一个函数调用嵌套,其结果和中缀表达式计算结果一致,抽象例子如下:

#include <stdio.h>

/*

数学表达式:(1 + 2) * 3 - 4

过程式和命令式:

int add = 1 + 2;
int mul = add * 3;
int sub = mul - 4;

函数式:

int result = subtract(multiply(add(1,2),3),4);

*/

int add(int x,int y);
int mul(int x,int y);
int sub(int x,int y);

int add(int base,int n) {
    return base + n;
}

int mul(int base,int n) {
    return base * n;
}

int sub(int base,int n) {
    return base - n;
}

int main(){
    int res = sub(mul(add(1,2),3),4);
    printf("(1 + 2) * 3 - 4 = %d",res);
}
const twoSum = (nums, target) => {
    const search = (index, complement, map) => {
        if (index >= nums.length) return [];
        if (map.has(complement)) return [map.get(complement), index];
        map.set(nums[index], index);
        return search(index + 1, target - nums[index], map);
    };

    return search(0, target - nums[0], new Map());
};
便宜 VPS vultr
最后修改:2024 年 05 月 21 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !