共计 8063 个字符,预计需要花费 21 分钟才能阅读完成。
函数式编程是一种历史悠久的编程范式。作为演算法,它的历史能够追溯到古代计算机诞生之前的 λ 演算,本文心愿带大家疾速理解函数式编程的历史、根底技术、重要个性和实际法令。在内容层面,次要应用 JavaScript 语言来形容函数式编程的个性,并以演算规定、语言个性、范式个性、副作用解决等方面作为切入点,通过大量演示示例来解说这种编程范式。同时,文末列举比拟一些此范式的优缺点,供读者参考。因为文章涵盖一些领域论常识,可能须要其余参考资料一起辅助浏览。
1. 前文回顾
在上篇中,咱们剖析了函数式编程的起源和根本个性,并通过每一个个性的示例来演示这种个性的实际效果。首先,函数式编程起源于数理逻辑,起源于 λ 演算,这是一种演算法,它定义一些根底的数据结构,而后通过归约和代换来实现更简单的数据结构,而函数自身也是它的一种数据。其次,咱们探讨了很多函数式编程的个性,比方:
- First Class
- 纯函数
- 援用通明
- 表达式
- 高阶函数
- 柯里化
- 函数组合
- point-free
- …
但咱们也指出了一个理论问题:不能解决副作用的程序是毫无意义的。咱们的计算机程序随时都在产生副作用。咱们程序外面有大量的网络申请、多媒体输入输出、外部状态、全局状态等,甚至在提倡“碳中和”的明天,电脑的发热量也是一个不容小觑的副作用。那么咱们应该如何解决这些问题呢?
2. 本文简介
本文通过深刻函数式编程的副作用解决及理论利用场景,提供一个学习和应用函数式编程的视角给读者。一方面,这种副作用治理形式是一种高级的形象模式,不易了解;另一方面,咱们在学习和应用函数式编程的过程中,简直都会遇到相似的副作用问题须要解决,是否解决这个问题也决定了一门函数式编程语言最终是否能走上胜利。
本文次要分为三个局部:
- 副作用解决形式
- 函数式编程的利用
- 函数式编程的优缺点比拟
3. 副作用解决:单子 Monad,一种不可避免的形象
下面说的,都是最根底的 JavaScript 概念 + 函数式编程概念。但咱们还留了一个“坑”。
如何去解决 IO 操作?
咱们的代码常常在和副作用打交道,如果要满足纯函数的要求,简直连一个需要都实现不了。不必急,咱们来看一下 React Hooks。React Hooks 的设计是很奇妙的,以 useEffect 为例:
在函数组件中,useState 用来产生状态,在应用 useEffect 的时候,咱们须要挂载这个 state 到第二个参数,而第一个参数给到的运行函数在 state 变更的时候被调用,被调用时失去最新的 state。
这外面有一个状态转换:
React Hooks 给咱们的启发是,副作用都被放到一个状态节点外面去被动触发,行程一个单向的数据流动。而实际上,函数式编程语言的确也是这么做的,把副作用包裹到一个非凡的函数外面。
如果一个函数既蕴含了咱们的值,又封装了值的对立操作,使得咱们能够在它限定的范畴内进行任意运算,那么,咱们称这种函数类型为 Monad。Monad 是一种高级别的思维形象。
3.1 什么是 Monad?
先思考一个问题,上面两个定义有什么区别?
num1是数字类型,而 num2 是对象类型,这是一个直观的区别。
不过,不仅仅如此。利用类型,咱们能够做更多的事。因为作为数字的 num1 是反对加减乘除运算的,而 num2 却不行,必须要把它视为一个对象 {val: 2},并通过属性拜访符num2.val 能力进行计算 num2.val + 2。但咱们晓得,函数式编程是不能扭转状态的,当初为了计算num2.val 被扭转了,这不是咱们冀望的,并且咱们应用属性操作符去读数据,更像是在操作对象,而不是操作函数,这与咱们的初衷有所背离。
当初咱们把 num2 当作一个独立的数据,并假如存在一个办法 fmap 能够操作这个数据,可能是这样的。
失去的还是对象,但操作通过一个纯函数 addOne 去实现了。
下面这个例子外面的 Num,实际上就是一个最简略的Monad,而fmap 是属于 Functor(函子) 的概念。咱们说函数就是从一个数据到另一个数据的映射,这里的 fmap 就是一个映射函数,在领域论外面叫做 态射(前面解说)。
因为有一个包裹的过程,很多人会把 Monad 看作是一个盒子类型。但 Monad 不仅是一个盒子的概念,它还须要满足一些特定的运算法则(前面波及)。
然而咱们间接应用数字的加减乘除不行吗?为什么肯定要 Monad 类型?
首先,fmap的目标是把数据从一个类型映射到另一个类型,而 JavaScript 外面的 map 函数实际上就是这个性能。
咱们能够认为 Array 就是一个 Monad 实现,map<T, K>把 Array< T > 类型映射到 Array< K > 类型,操作依然在数组领域,数组的值被映射为新的值。如果用 TypeScript 来示意,会不会更清晰一点?
看起来 Monad 只是一个实现了 fmap 的对象(Functor类型,mappable 接口)而已。但 Monad 类型不仅是一个Functor,它还有很多其余的工具函数,比方:
- bind 函数
- flatMap 函数
- liftM 函数
这些概念在学习 Haskell 时能够遇到,本文不作过多提及。这些额定的函数能够帮忙咱们操作被封装起来的值。
3.2 领域、群、幺半群
领域论是一种钻研形象数学模式的迷信,它把咱们的数学世界形象为两个概念:
- 对象
- 态射
为什么说这是一种 模式 上的形象呢?因为很多数学的概念都能够被这种模式所形容,比方汇合,对汇合领域来说,一个汇合就是一个领域对象,从汇合 A 到汇合 B 的映射就是汇合的态射,再细化一点,整数汇合到整数汇合的加减乘操作形成了整数汇合的态射(除法会产生整数汇合无奈示意的数字,因而这里排除了除法)。又比方,三角形能够被代数示意,也能够用几何示意、向量示意,从代数示意到几何示意的运算就能够视为三角形领域的一种态射。
总之,对象形容了一个领域中的元素,而态射形容了针对这些元素的操作。领域论不仅能够利用到数学迷信外面,在其余迷信外面也有一些利用,实际上,领域论就是咱们形容主观世界的一种形式(形象模式)。
绝对应的,函子就是形容一个领域对象和另一个领域对象间关系的态射,具体到编程语言中,函子是一个帮忙咱们映射一个领域元素(比方Monad)到另一个领域元素的函数。
群论(Group)钻研的是 群这种代数构造,怎么去了解群呢?比方一个三角形有三个顶点 A/B/C,那么咱们能够示意一个三角形为ABC 或者 ACB,三角形还是这个三角形,然而从ABC 到ACB肯定是通过了某种变换。这就像领域论,三角形的示意是领域对象,而一个三角形的示意变换到另一个模式,就是领域的态射。而咱们说这些三角形示意形式的汇合为一个 群。群论次要是钻研变换关系,群又能够分为很多品种,也有很多法则个性,这不在本文钻研范畴之内,读者能够自行学习相干内容。
迷信解释一个 Monad 为自函子领域上的幺半群。如果没有学习群论和领域论的话,咱们是很难了解这个解释的。
简略来说先固定一个正方形 abcd,它和它的几何变换形式(旋转 / 逆时针旋转 / 对称 / 中心对称等)造成的其余正方形一起形成一个群。从这个角度来说,群钻研的事物是同一类,只是性质稍有不一样(态射后)。
另外一个了解群的概念就是 自然数(形成一个群)和加法(群的二元运算,且满足结合律,半群)。
到此,咱们能够了解 Monad 为:
- 满足自函子运算(从 A 领域态射到 A 领域,fmap 是在本人空间做映射)。
- 满足含幺半群的结合律。
很多函数式编程外面都会实现一个 Identity 函数,理论就是一个幺元素。比方 JavaScript 中对 Just 满足二元结合律能够这么操作:
3.3 Monad 领域:定律、折叠和链
咱们要在一个更大的空间上探讨这个领域对象(Monad)。就像 Number 封装了数字类型,Monad 也封装了一些类型。
Monad须要满足一些定律:
- 结合律:比方 a · b · c = a · (b · c)。
- 幺元:比方 a · e = e · a = a。
一旦定义了 Monad 为一类对象,fmap为针对这种对象的操作,那么定律咱们能够很容易证实:
咱们能够通过 Monad Just 上挂载的操作来对数据进行计算,这些运算是限定在了 Just 上的,也就是说你只能失去 Just(..) 类型。要获取原始数据,能够基于这个定义一个 fold 办法。
fold(折叠,对应能力咱们称为 foldable)的意义在于你能够将数据从一个特定领域映射到你的罕用领域,比方面向对象语言的 toString 办法,就是把数据从对象域转换到字符串域。
JavaScript 中的 Array.prototype.reduce 其实就是一个 fold 函数,它把数据从 Array 领域映射到其余领域。
一旦数据类型被咱们锁定在了 Monad 空间(领域),那咱们就能够在这个领域内间断调用 fmap(或者其余这个空间的函数)来进行值操作,这样咱们就能够 链式解决 咱们的数据。
3.4 Maybe 和 Either
有了 Just 的概念,咱们再来学习一些新的 Monad 概念。比方Nothing。
Nothing示意在 Monad 领域上没有的值。和 Just 一起正好形容了所有的数据状况,合称为 Maybe,咱们的Maybe Monad 要么是Just,要么是Nothing。这有什么意义呢?
其实这就是模仿了其余领域内的“有”和“无”的概念,不便咱们模仿其余编程范式的空值操作。比方:
这种状况下咱们须要去判断 x 和y是否为空。在 Monad 空间中,这种状况就很好示意:
咱们在 Monad 空间中打消了烦人的!== null 判断,甚至打消了三元运算符。所有都只有函数。理论应用中一个 Maybe 要么是 Just 要么是 Nothing。因而,这里用Maybe(..) 结构可能让咱们难以了解。
如果非要了解的话,能够了解 Maybe 为Nothing和 Just 的抽象类,Just和 Nothing 形成这个抽象类的两个实现。理论在函数式编程语言实现中,Maybe的确只是一个类型(称为代数类型),具体的一个值有具体类型 Just 或Nothing,就像数字能够分为有理数和无理数一样。
除了这种值存在与否的判断,咱们的程序还有一些分支构造的形式,因而咱们来看一下在 Monad 空间中,分支状况怎么去模仿?
假如咱们有一个代数类型 Either,Left 和Right别离示意当数据为谬误和数据为正确状况下的逻辑。
这样,咱们就能够应用“函数”来代替分支了。这里的 Either 实现比拟毛糙,因为 Either 类型应该只在 Monad 空间。这里退出了布尔常量的判断,目标是好了解一些。其余的编程语言个性,在函数式编程中也能找到对应的影子,比方循环构造,咱们往往应用函数递归来实现。
3.5 IO 的解决形式
终于到 IO 了,如果不能解决好 IO,咱们的程序是不健全的。到目前为止,咱们的Monad 都是针对数据的。这句话对也不对,因为函数也是一种数据(函数是第一公民)。咱们先让 Monad Just 能存储函数。
你能够设想为 Just 减少了一个抽象类实现,这个抽象类为:
这个抽象类咱们称为“利用函子”,它能够保留一个函数作为外部值,并且应用 apply 办法能够把这个函数作用到另一个 Monad 上。到这里,咱们齐全能够把 Monad 之间的各种操作(接口,比方 fmap 和apply)视为契约,也就是数学上的态射。
当初,如果咱们有一个单子叫IO,并且它有如下体现:
咱们把这种类型的 Monad 称为 IO,咱们在IO 中解决打印(副作用)。你能够把之前咱们学习到的类型合并一下,失去一个示例:
通常一个程序会有一个主入口函数 main,这个 main 函数返回值类型是一个 IO,咱们的副作用当初全在 IO 这个领域下运行,而其余操作,都能够放弃污浊(类型运算)。
IO 类型让咱们能够在 Monad 空间解决那些烦人的副作用,这个 Monad 类型和Promise(限定副作用到 Promise 域解决,可链式调用,可用 then 折叠和映射)很像。
4. 函数式编程的利用
除了下面咱们提到的一些示例,函数式编程能够利用到更广的业务代码开发中,用来代替咱们的一些根底业务代码。这里举几个例子。
4.1 设计一个申请模块
用这种形式构建的模块,组合和复用性很强,你也能够利用 lodash 的其余库对 req 做一个其余革新。咱们调用业务代码的时候只管传递 params,分支校验和谬误查看就教给 validate.js 外面的高阶函数就好了。
4.2 设计一个输入框
这个例子也是来源于前端常见的场景。咱们应用函数式编程的思维,把多个看似不相干的函数进行组合,失去了业务须要的 subscribe 函数,但同时,下面的任意一个函数都能够被用于其余性能组合。比方 callback 函数能够间接给 dom 回调,listenInput 能够用于任意一个 dom。
这种通过高阶组件不停组合失去最终后果的形式,咱们能够认为就是函数式的。(只管它没有像上一个例子一样引入 IO/Monad 等概念)
4.3 超长文本省略:Ramdajs 为例
这个也是常见的前端场景,当文本长度大于 X 时,显示省略号,这个实现应用 Ramdajs。这个过程中你就像是在搭积木,很容易就把业务给“搭建”实现了。
5. 函数式编程库、语言
函数式编程的库能够学习:
- Ramda.js:函数式编程库
- lodash.js:函数工具
- immutable.js:数据不可变
- rx.js:响应式编程
- partial.lenses:函数工具
- monio.js:函数式编程工具库 /IO 库
- …
你能够联合起来应用。上面是 Ramda.js 示例:
而纯函数式语言,有很多:
- Lisp 代表软件 emacs…
- Haskell 代表软件 pandoc…
- Ocaml …
- …
6. 总结
函数式编程并不是什么“黑科技”,它曾经存在的工夫甚至比面向对象编程更长远。心愿本文能帮忙大家了解什么是函数式编程。
当初咱们来回顾先览,实际上,函数式编程也是程序实现形式的一种,它和面向对象是必由之路的。在函数式语言中,咱们要构建一个个小的根底函数,并通过一些通用的流程把他们粘合起来。举个例子,面向对象外面的继承,我在函数式编程中能够应用组合 compose 或者高阶函数 hoc 来实现。
只管在实现上是等价的,但和面向对象的编程范式比照,函数式编程有很多长处值得大家去尝试。
6.1 长处
除了下面提到的格调和个性之外,函数式编程绝对其余编程范式,有很多长处:
- 函数污浊 程序有更少的状态量,编码心智累赘更小。随着状态量的减少,某些编程范式构建的软件库代码复杂度可能呈几何增长,而函数式编程的状态量都收敛了,对软件复杂度带来的影响更小。
- 援用透明性 能够让你在不影响其余性能的前提下,降级某一个特定性能(一个对象的援用须要改变的话,可能牵一发而动全身)。
- 高度可组合 函数之间复用不便(须要关注的状态量更少),函数的性能降级革新也更容易(高阶组件)。
- 副作用隔离 所有的状态量被收敛到一个盒子(函数)外面解决,关注点更加集中。
- 代码简洁 / 流程更清晰 通常函数式编程格调的程序,代码量比其余编程格调的少很多,这得益于函数的高度可组合性以及大量的欠缺的根底函数,简洁性也使得代码更容易保护。
- 语义化 一个个小的函数别离实现一种小的性能,当你须要组合下层能力的时候,根本能够依照函数语义来进行疾速组合。
- 惰性计算 被组合的函数只会生成一个更高阶的函数,最初调用时数据才会在函数之间流动。
- 跨语言统一性 不同的语言,仿佛都听从相似的函数式编程范式,比方 Java 8 的 lambda 表达式,Rust 的 collection、匿名函数;而面向对象的实现,不同语言可能千差万别,函数式编程的统一性让你能够难受地跨语言开发。
- 要害畛域利用 因为函数式编程状态少、代码简洁等特点,使得它在交互简单、安全性要求高的畛域有重要的利用,像 Lisp 和 Haskell 就是因上一波人工智能热而火起来的,起初也在一些非凡的畛域(银行、水利、航空航天等)失去了较大规模的利用。
- …
6.2 有余
当然,函数式编程也存在一些不足之处:
- 平缓的学习曲线 面向对象和命令式编程范式都是贴近咱们的日常习惯的形式,而函数式编程要更形象一些,要想更好地治理副作用,你可能须要学习很多新的概念(响应式、Monad 等),这些概念入门很难,而且是一个长期积攒的过程。
- 可能的调用栈溢出问题 惰性计算在一些电脑或特种程序架构上可能有函数调用栈谬误(超长调用链、超长递归),另外许多函数式编程语言须要编译器反对尾递归优化(优化为循环迭代)以失去更好的性能。
- 额定的形象累赘 当程序有大量可变状态、副作用时,用函数式编程可能造成额定的形象累赘,我的项目开发周期可能会缩短,这时可能用其余形象形式更好(比方 OOP)。
- 数据不变性的问题 为了数据不变,运行时可能会构建生成大量的数据正本,造成工夫和空间耗费更大,拖慢性能;同时数据不可变性可能会造成构建一些根底数据结构的时候语法不简洁,性能也更差(比方 LinkedList、HashMap 等数据结构)。
- 语义化的问题 往往为了开发一个性能,去造许多的根底函数,大量业务组件想要语义化的命名,也会带给开发人员很多累赘;并且性能形象能力因人而异,公共函数往往不够专用或者适度设计。
- 生态问题 函数式编程在工业生产畛域因其抽象性和性能带来的问题,被许多开发者拒之门外,一些特定性能的解决方案也更小众(相比其余编程范式),生态也始终比拟小,这成为一些新的开发人员学习和应用函数式编程的又一个微小阻碍。
- …
日常业务开发中,往往咱们须要舍短取长,在适宜的畛域用适宜的办法 / 范式。大家只有要记住,软件开发并没有“银弹”。
7. FAQ
Q:你感觉 Promise 是不是一种 Monad IO 模型?
A:我认为是的。纯函数是没有异步概念的,Promise 用了一种很棒的形式把异步和 IO 转化为了.then 函数。你依然能够在.then 函数中写纯正的函数,也能够在.then 函数中调用其余的 Promise,这就和 IO Monad
的行为十分像。
Q:你违心在生产中应用 Haskell/Lisp/Clojure 等纯函数式语言吗?
A:不管是否违心应用,当初很多语言都开始引入函数式编程语法了。并不是说函数式编程肯定是优良的,但它至多没有那么恐怖。有一点能够必定的是,学习函数式编程能够扩大咱们的思维,减少咱们看问题的角度。
Q:有没有一些能够预感的益处?
A:有的。比方强制你写代码的时候去关注状态量(多少、是否援用值、是否变更等),这或多或少能够帮忙你写代码的时候缩小状态量的应用,也缓缓地能复合一些状态量,写出更简洁的代码。
Q:函数式编程能给业务带来什么益处?
A:业务拆分的时候,函数式的思维是单向的,咱们会通过实现,想到它对应须要的根底组件,并递归地思考上来,性能实现从最小粒度开始,下层逐渐通过函数组合来实现。相比于面向对象,这种形式在组合上更不便简洁,更容易把复杂度升高,比方面向对象中可能对象之间的互相援用和调用是没有限度的,这种模式带来的是思考逻辑的时候思维会发散。
这种比照在业务简单的状况下更加显著,面向对象必须要优良的设计模式来实现控制代码复杂度增长不那么快,而函数式编程大多数状况下都是单向数据流 + 根底工具库就缩小了大量的复杂度,而且产生的代码更简洁。
8. 作者简介
豪杰,美团到家研发平台 / 医药技术部前端工程师。
9. 参考文献
- 维基百科:函数式编程 /lambda 演算 / 领域论 / 集合论 / 群论。
- Github:getify/Functional-Light-JS
- 《Learn You A Haskell For Great Good!》
- 《Deep JavaScript》
- 其余:各类在线博客
浏览美团技术团队更多技术文章合集
前端 | 算法 | 后端 | 数据 | 平安 | 运维 | iOS | Android | 测试
| 在公众号菜单栏对话框回复【2021 年货】、【2020 年货】、【2019 年货】、【2018 年货】、【2017 年货】等关键词,可查看美团技术团队历年技术文章合集。
| 本文系美团技术团队出品,著作权归属美团。欢送出于分享和交换等非商业目标转载或应用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者应用。任何商用行为,请发送邮件至 [email protected] 申请受权。