为什么要学习函数式编程?因为如果你手里只有锤子,看什么都像钉子

简介: 函数式编程是一种“编程范式”,也就是如何编写程序的方法论,其主要思想是把运算过程尽量写成一系列嵌套的函数调用。那么在函数式编程比较火爆的今天,我们为什么要学习它呢?学习函数式编程究竟能为我们带来什么呢?本文或许能给你一点启发。
摘要:函数式编程是一种“编程范式”,也就是如何编写程序的方法论,其主要思想是把运算过程尽量写成一系列嵌套的函数调用。那么在函数式编程比较火爆的今天,我们为什么要学习它呢?学习函数式编程究竟能为我们带来什么呢?本文或许能给你一点启发。

视频回顾地址: https://yq.aliyun.com/video/play/1415
PPT下载地址: https://yq.aliyun.com/download/2571

演讲嘉宾简介
陶云峰,阿里云高级技术专家,上海交通大学理论计算机科学博士,专注数据存储、分布式系统与计算等领域,写了20多年程序。2000年参加ACM/ICPC大赛,实现亚洲队伍进World Final前十的突破。

以下内容根据演讲视频以及PPT整理而成。

首先实现一个sum函数,在sum函数中传入一个vector<int>,sum所做的工作就是将vector<int>里面的int通过累加器加到一起并返回。
19c3cc3fb8b2eb45322f1da34940ba5329b9835e
如下代码中实现了一个累乘器。同样传入一个vector<int>,product所做的工作就是将vector<int>里面的int通过累乘起来并返回。
9a413c15d690e0167e78426e27bdfd39753f43e0
如下代码实现了一个concat,其所做的就是将vector<string>中的每一个string拼接到一起形成一个大的string并返回,其做法与上述的sum和product类似。
1f93b3209075363b357b95ab27a89356259a1105
可以看到上述所做的累加器、累乘器以及字符串拼接函数都具有相同的结构,那么需要思考如何将其抽象出来。从面向对象的角度来讲,这就是一个策略模式,需要将策略和执行策略的上下文分离开,从函数式编程的角度来讲,可以通过reduce函数来抽象代码结构。
5caff7eae029e3865eb3533e48763363126dc2d6
reduce函数具有三个参数,最后一个参数是待处理的数组,其第一个参数是一个函数,该函数接受一个累积的变量和数组中某一个元素,就可以将元素累积到结果上,此外还需要一个初始值init。过程可以被抽象成如上述代码所示。这样sum的实现只需要调用reduce<int>并且初始值赋0,concat的实现只需要调用reduce<string>并且使得初始值为空串即可。这里用到了函数式编程中的技巧——高阶函数。高阶函数在这里面就是把一个函数作为参数传递给另外一个函数。

在下列示例代码中定义了一个树形结构,每个树节点上面都有整数值mPayload,并且还有零到若干个子树可以放到vector里面。现在想要将这颗树上所有节点的值全部加到一起,根据上述的做法可以知道,在实现时可以使用一个reduceTree。reduceTree同样接受三个参数,reduce用的函数、初始值和树的根节点。整个过程大致就是将累积变量定义好,将根节点的mPayload作用上去,然后将每个子树reduce好的结果作用到累积器上。此时想要实现树上节点的值全部加在一起的sum可以通过在累积函数参数上传递一个加法,初始值传递一个0即可。
6cd324ada4980d599aeae6a2eb5bf1ae56c46e96
这样就会发现sumTree函数和sum函数内部传递的东西是一模一样的,那么如何将这一部分抽象出来呢?其实可以使用bind函数。首先把加法变成一个函数,然后将add和0绑定到reduce<int>和reduceTree<int>上面去就可以得到所需要的sum和sumTree。这里值得注意的就是bind也是高阶函数的一种,其特征是返回值是函数。通过bind这样的高阶函数可以将代码更进一步地简化。
ea2f045b710e9a791297ac487361ab9c39b83e40

牛顿-拉夫森迭代
平方根有很多种算法,其中一种就是牛顿-拉夫森迭代,这种方法是一种非常高效的迭代方法。其大致就是如果想要对于x求平方根,那么可以根据迭代的前一项使用这个公式算法来得到后一项。如下代码所实现的就是牛顿-拉夫森迭代,所传入的两个参数分别是所要求平方根的数值和所要误差。在代码中首先定义一个初始值,每次使用牛顿-拉夫森公式计算下一个值,如果前后两个值的偏差小于传入的要求误差就可以返回当前值,否则当前值就变成下一个值继续进行下一次迭代。
95e19156da28910cd27be277978d3352f7cfb97f

求导数
求导数其实就是不停地求斜率,当h逼近0的时候,斜率也就逼近f(x)的导数了。在代码实现中,参数分别是需要求导的函数、函数求导的位置以及误差。h从1.0开始,每一次都会折半,比较当前的斜率和下一次的斜率,如果前后两个斜率误差足够小,结果就可以返回了,否则就继续执行。
ad3b7014bb78aba933d55a66782d983cac5c203d
这样大家就会发现求导数和牛顿-拉夫森迭代算法都有相同的结构,总体而言,就是都有一个循环迭代,另外循环的终止条件都是由误差决定的。两者的细微差别就是牛顿-拉夫森迭代算法的迭代变量最终返回的结果就是给用户看到的结果,而求导数的迭代变量是h,最终看到是使用h计算出的斜率,而不是h本身。那么如何抽象上述两个算法呢?

方案一
下面定义了within1函数,其参数分别是误差和所要传递的表观函数和状态转移函数。每一次迭代中,状态转移函数负责将当前这个状态变成下一个状态,而表观函数则负责将状态转化成用户需要看到的值,最后利用用户需要看到的前后两个值的差来判断其误差,如果误差足够小就返回,否则就继续迭代。
833953089887c2ea51cc3e4f8069e2783c3af4ea
对于牛顿-拉夫森迭代来讲,其状态转移函数就直接使用牛顿-拉夫森函数即可,其表观函数实际上则不需要,这里可以放置一个恒等函数,输入什么就输出返回什么。
db22759055e312bdb209f080096126f946bff0fb
对于求导数而言,状态转移函数就是每次取半,表观函数就是求斜率。这里的示例代码中之所以使用的"sin_"是因为sin()函数是一个重载的函数,其有多重重载方案,所以如果在调用时直接写"sin",编译器无法知道重载哪一个版本,这也是重载函数不如模板特化函数的一点,所以重载函数需要做一个lambda表达式将其包进里面,通过输入的类型为double的x告诉编译器要使用double版本的sin()函数。
478d8f577b4cccf35111735f7c5a72c1f8d0b388
从面相对象的角度来讲,方案一的within1函数实际上是一个strategy pattern,配合转移和表观两个strategy。其可以有一些扩展,比如内部状态不一定是double,在设计模式中有一个叫做memento pattern,当然对于C++而言可以使用模板。如果状态转移和表观这两件事情紧耦合,可以使用抽象工厂模式,如果状态转移和表观是松耦合的,则可以使用原型模式。那么是不是这样就足够好了呢?其实并不是的,可以看到无论哪些模式用上去都是比较复杂的,没有within1函数这么简洁。而within1函数简洁的核心之处就在于其使用了高阶函数。那么是不是within1就是最好的方法呢?也不是的,函数式编程又提供了另外一种思考的角度。


方案二

在函数式编程中,可以将牛顿-拉夫森迭代视作一个无限长的序列,再截断不需要的尾巴。而问题是计算机资源是有限的,不可能计算出一个无限长的序列,所以需要Lazy sequence。Lazy sequence逻辑上是一个无限长的序列,但是其元素只有需要的时候才会实际产生出来。在函数式编程中,Lazy sequence就是一个有状态但是无参数的函数,每一次调用都会返回当前的状态并将自己的状态迁移到下一个。下列代码中使用了值捕获,产生一个内部状态,并且加上mutable使得内部状态可以被改变。
fb19a8cb7017e36b1fede8e4e6daf920cfb8148b
此外还需要进行截断,需要遍历Lazy sequence,并在遍历过程中截断,这也是下列代码中within2所做的事情,每次迭代都会取一个值,当前一个值和后一个值的误差足够小之后就结束。
c92f95c1b1a9b04ed63b17f18fd122eb0d0c4be9
牛顿-拉夫森迭代算法就可以变成如下代码的形式,函数传递一个x并传递一个误差值,每次向下走的时候就是牛顿-拉夫森迭代,这里捕获的就是x,然后产生一个无限长的序列,把序列和误差精度传入到within2函数中去。
326b20d35d305629bcbc88060b39483c7c3f22eb
在这样的方案中应该如何计算导数呢?大家可以重新看一下求导数的公式,这里的limit记号所代表的是有一个h接近0无限序列,对这个序列求出了一个斜率的序列,当h逼近0的时候,斜率也会逼近一个值,也就是所需要的导函数。从实现的角度而言,将极限逼近的过程视作一个无限长的序列。将一个无限长序列变换到另一个无限长的序列,也就是从h的序列变成斜率的序列,然后截断。其核心就是将一个无限长序列变换到另一个无限长的序列,也就是map所实现的。在函数式编程的语境下面,map就是把一个序列变换成另外一个序列,这与过程式编程中的数据结构map是不同的。对于map而言,其结果仍然是一个无限长的序列,所以其也是一个无参数但是有内部状态的函数,其接受一个变换的函数和无限长序列,并返回一个函数。该函数无参数,其所作的事情就是将输入的序列取一个值,把函数作用上去之后返回。
deaa9579d9a8ff0080a0384d2b9953c9aec649b6
在方案二里面的实现求导数如下所示,首先每次将h取半,通过迭代得到一个无限长的序列seq,然后切斜率函数将其作用在原来的无限长序列上面得到另一个无限长序列,然后使用eps误差进行截断。代码的实现是相当直白的,基本上就是按照limit记号来写的。
b7ae9d8fb43a37090a0ae821110f365eebaad85c
在这个方案中引入了一个无限长序列的概念,而无限长序列在计算机中是无法实现的,所以将其转换成了一个Lazy sequence,除了与业务相关within2函数中的误差属于业务概念外,iterate和map都不是业务的概念。方案二中引入的新增概念就是Lazy sequence和within2,所以方案二比方案一更为直观,需要引入更少的概念,不需要表观和转移,尤其是对于牛顿-拉夫森迭代算法这样的情况,在方案一牛顿-拉夫森迭代算法的表观函数就是一个恒等函数,算法本身不需要表观概念,但是为了套在within1框架中,所以强制搞出来一个表观函数,而在方案二中却是不需要的。总而言之就是方案二比方案一代码实现更简洁,更贴近业务。

总结
总结而言,在本次分享中主要介绍了以下四点:
1. 高阶函数。函数可以作为参数,也可以作为返回值。
2. Lazy sequence。逻辑上的无限长序列,实现中是一个有状态无参数的函数。
3. 新的“胶水”。函数式编程提供了新的建模思路,新的胶合代码组件的方法。“胶水”不同,分解问题的方式也不同。
4. “没有银弹”。如果你手里只有锤子,看什么都像钉子。学习函数式编程是为了丰富你的武器库。
相关文章
|
8月前
|
消息中间件 安全 物联网
海量接入、毫秒响应:易易互联基于 Apache RocketMQ + MQTT 构筑高可用物联网消息中枢
易易互联科技有限公司是吉利集团旗下专注于换电生态的全资子公司,致力于打造安全、便捷、便宜的智能换电网络。公司依托吉利GBRC换电平台,基于电池共享与车辆全生命周期运营,已布局超470座换电站,覆盖40多个城市,计划2027年达2000座。面对海量设备高并发连接、高实时性要求及数据洪峰挑战,易易互联采用阿里云MQTT与RocketMQ构建高效物联网通信架构,实现稳定接入、低延迟通信与弹性处理,全面支撑其全国换电网络规模化运营与智能化升级。
500 1
海量接入、毫秒响应:易易互联基于 Apache RocketMQ + MQTT 构筑高可用物联网消息中枢
|
9月前
|
人工智能 监控 Serverless
相比于直接消费 MCP 服务,您的企业可能更需要一个专属的 MCP 服务中心
MCP(Model Control Protocol)作为AI应用上下文工程中的关键组成部分,正广泛应用于企业AI转型实践。企业开发人员通过Cursor、Cline、灵码等AI工具使用MCP,结合自定义MCP实现创新,但也面临生产发布、沉淀复用等挑战。Function AI提供完整的企业级MCP解决方案,通过标准化流程解决MCP构建与发布问题,并通过MCP市场模板打造企业专属服务中心,提升复用效率。方案支持快速部署、测试及集成,助力企业高效构建智能化体系。
|
9月前
|
人工智能 运维 安全
科技云报到:Agent应用爆发,谁成为向上托举的力量?
AI正加速迈入Agent时代,具备推理、规划与工具调用能力的智能体,正重塑企业应用形态。2025年,AI Agent基础设施迎来爆发,云厂商纷纷推出新一代技术架构,突破长时运行、状态管理、安全隔离等关键难题,推动Agent从实验室走向千万级企业场景。算力、工具链与安全等挑战逐步被攻克,AI Agent正成为企业智能化转型的核心驱动力。
651 0
三角函数中的正弦、余弦、正切、余切、正割、余割函数性质及常用公式
三角函数中的正弦、余弦、正切、余切、正割、余割函数性质及常用公式
2854 0
三角函数中的正弦、余弦、正切、余切、正割、余割函数性质及常用公式
|
存储 Java 数据库连接
|
7天前
|
Shell API 开发工具
Claude Code 快速上手指南(新手友好版)
AI编程工具卷疯啦!Claude Code凭借任务驱动+终端原生的特性,成了开发者的效率搭子。本文从安装、登录、切换国产模型到常用命令,手把手带新手快速上手,全程避坑,30分钟独立用起来。
2507 13
|
20天前
|
人工智能 JSON 供应链
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
LucianaiB分享零成本畅用JVS Claw教程(学生认证享7个月使用权),并开源GeoMind项目——将JVS改造为科研与产业地理情报可视化AI助手,支持飞书文档解析、地理编码与腾讯地图可视化,助力产业关系图谱构建。
23547 13
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
|
5天前
|
人工智能 开发工具 iOS开发
Claude Code 新手完全上手指南:安装、国产模型配置与常用命令全解
Claude Code 是一款运行在终端环境中的 AI 编程助手,能够直接在命令行中完成代码生成、项目分析、文件修改、命令执行、Git 管理等开发全流程工作。它最大的特点是**任务驱动、终端原生、轻量高效、多模型兼容**,无需图形界面、不依赖 IDE 插件,能够深度融入开发者日常工作流。
1923 3
|
7天前
|
人工智能 JSON BI
DeepSeek V4-Pro 接入 Claude Code 完全实战:体验、测试与关键避坑指南
Claude Code 作为当前主流的 AI 编程辅助工具,凭借强大的代码理解、工程执行与自动化能力深受开发者喜爱,但原生模型的使用成本相对较高。为了在保持能力的同时进一步降低开销,不少开发者开始寻找兼容度高、价格更友好的替代模型。DeepSeek V4 系列的发布带来了新的选择,该系列包含 V4-Pro 与 V4-Flash 两款模型,并提供了与 Anthropic 完全兼容的 API 接口,理论上只需简单修改配置,即可让 Claude Code 无缝切换为 DeepSeek 引擎。
1811 1

热门文章

最新文章