当前位置: 首页 > Web前端 > HTML

深入理解函数式编程(下)

时间:2023-03-28 18:05:06 HTML

函数式编程是一种历史悠久的编程范式。作为一种算法,它的历史可以追溯到现代计算机诞生之前的λ演算。本文希望能带你快速了解函数式编程的历史、基本技术、重要特性和实用规则。在内容层面,主要使用JavaScript语言描述函数式编程的特性,通过大量的演示实例以演算规则、语言特性、范式特性、副作用处理为切入点来讲解编程范式.同时在文末列出并比较了该范式的一些优缺点,以供读者参考。由于文章涵盖了一些范畴论知识,可能需要其他参考资料来辅助阅读。1.上一篇文章回顾在上一篇文章中,我们分析了函数式编程的起源和基本特征,并通过每个特征的例子展示了该特征的实际效果。首先,函数式编程起源于数理逻辑和lambda演算,是一种算法,定义一些基本的数据结构,然后通过归约和代入实现更复杂的数据结构,而函数本身也是一种数据.其次,我们讨论了函数式编程的很多特点,比如:FirstClasspurefunctionreferenceTransparentexpressionHigher-orderfunctionCurriedfunctioncompositionpoint-free...但我们也指出了一个实际问题:不能处理副作用的程序是没有意义的.我们的计算机程序一直在产生副作用。我们的程序中有大量的网络请求、多媒体输入输出、内部状态、全局状态等。即使在提倡“碳中和”的今天,电脑产生的热量也是不可小觑的副作用。那么我们应该如何处理这些问题呢?2.本文简介本文通过深入函数式编程的副作用和实际应用场景,为读者提供一个学习和使用函数式编程的视角。一方面,这种副作用管理方法是一种高级抽象形式,不易理解;另一方面,在学习和使用函数式编程的过程中,我们几乎总是会遇到类似的需要解决的副作用问题。我们能解决这个问题吗?这个问题也决定了函数式编程语言最终能否成功。本文主要分为三个部分:函数式编程的优缺点和函数式编程在副作用处理中的应用三、副作用处理:monad,一个不可避免的抽象以上就是最基本的JavaScript概念+函数式编程概念。但是我们还是留下了一个“坑”。如何处理IO操作?我们的代码经常处理副作用。如果我们要满足纯函数的需求,一个需求都很难完成。别着急,让我们来看看ReactHooks。ReactHooks的设计非常巧妙。以useEffect为例:在函数组件中,useState用于生成状态。在使用useEffect的时候,我们需要把这个状态挂载到第二个参数上,第一个参数给状态变化时调用running函数,调用时获取最新的状态。这里面有一个状态转换:ReactHooks给我们的启示是副作用都放在一个状态节点中被动触发,旅程是单向的数据流。事实上,函数式编程语言正是这样做的,将副作用包装在一个特殊的函数中。如果一个函数不仅包含了我们的值,而且还封装了对值的统一操作,让我们可以在它限定的范围内进行任意操作,那么我们称这类函数为Monad。单子是思想的高级抽象。3.1什么是单子?先想一个问题,下面两个定义有什么区别?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将Array类型映射到Array类型。操作还是在数组的范畴,数组的值被映射为一个新的值。如果用TypeScript表达会不会更清楚?看起来Monad只是一个实现了fmap的对象(Functor类型,可映射接口)。但是Monad类型不仅仅是一个Functor,它还有很多其他的实用函数,比如:bind函数,flatMap函数,liftM函数。这些概念在学习Haskell时都会遇到,本文不再过多提及。这些额外的函数帮助我们操作包装值。3.2范畴、群和幺半群范畴论是一门研究抽象数学形式的科学。它将我们的数学世界抽象为两个概念:对象态射。为什么这是一个正式的抽象?因为很多数学概念都可以用这种形式来描述,比如集合,对于集合的范畴来说,集合就是范畴对象,集合A到集合B的映射就是集合的态射。集合对整数集的加、减、乘构成了整数集的态射(除法产生的数不能用整数集表示,所以这里排除除法)。再例如,三角形可以用代数、几何或矢量表示。从代数表示到几何表示的运算可以看作是三角形范畴的一个态射。总之,对象描述类别中的元素,而态射描述对这些元素的操作。范畴论不仅可以应用于数学科学,在其他科学中也有一定的应用。事实上,范畴论是我们描述客观世界的一种方式(抽象形式)。相应地,函子是描述一个范畴对象与另一个范畴对象之间关系的态射。具体来说,在编程语言中,函子是帮助我们将一个类别元素(例如Monad)映射到另一个类别元素的函数。群论研究群的代数结构。如何理解群体?比如一个三角形有三个顶点A/B/C,那么我们可以把一个三角形表示为ABC或者ACB,这个三角形仍然是同一个三角形,但是从ABC到ACB肯定有一些变换。这好比范畴论,三角形的表示就是范畴对象,三角形表示转化为另一种形式就是范畴的态射。我们说这些三角形表示的集合是一个群。群论主要是研究变换关系。群体可分为多种类型,具有多种规律性特征。这不在本文的讨论范围之内。读者可自行了解相关内容。科学将Monad解释为自函子类别上的幺半群。如果不研究群论和范畴论,我们很难理解这种解释。简单的说,先固定一个正方形abcd,它和其他由它几何变换(旋转/逆时针旋转/对称/中心对称等)形成的正方形组成一个群。从这个角度来看,群研究的事物是同类的,只是性质略有不同(在态射之后)。理解群的另一个概念是自然数(构成群)和加法(群的二元运算,满足结合律,半群)。至此,我们可以将Monad理解为:满足自函子操作(从A类态射到A类,fmap在自己的空间映射)。满足与幺半群的结合律。很多函数式编程都会实现一个Identity函数,其实就是一个unit元素。例如,在JavaScript中,满足Just的二元结合律可以这样进行:3.3Monad类别:Laws,FoldsandChains我们想在更大的空间里讨论这个类别对象(Monad)。就像Number封装了数值类型一样,Monad也封装了一些类型。Monad需要满足一些规律:结合律:如a·b·c=a·(b·c)。酉:例如a·e=e·a=a。一旦将Monad定义为一类对象,而fmap就是对此类对象的操作,那么我们就可以很容易地证明这个规律:我们可以通过挂载在Monad上的操作来计算数据Just,而这些操作仅限于Just,这意味着你只能得到Just(..)类型。要获取原始数据,可以在此基础上定义一个折叠方法。fold(折叠,对应的能力我们称之为foldable)的意思是,你可以将数据从一个特定的类别映射到你常用的类别,比如面向对象语言的toString方法,将数据从对象域转换为字符串域。JavaScript中的Array.prototype.reduce其实就是一个fold函数,将Array类中的数据映射到其他类中。一旦数据类型被我们锁定在Monad空间(类别)中,那么我们就可以在这个类别中不断调用fmap(或这个空间中的其他函数)进行值操作,这样我们就可以链式处理我们的数据。3.4Maybe和Either有了Just的概念,我们再学习一些Monad的新概念。如无。Nothing表示Monad类别中不存在的值。和Just一起,只是描述了所有的数据情况,统称为Maybe,而我们的MaybeMonad要么是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.5IO的处理方法终于到了IO。如果我们不能很好地处理IO,我们的程序就是不健全的。到目前为止,我们的monad都是关于数据的。这句话是对是错,因为函数也是一种数据(函数是第一公民)。我们先让Monad能够存储函数。你可以想象给Just增加一个抽象类的实现,这个抽象类是:这个抽象类我们称之为“applicationfunctor”,它可以将一个函数保存为一个内部值,并使用apply方法将这个函数应用到另一个Monad上。至此,我们完全可以把Monad之间的各种操作(接口,如fmap、apply)看成契约,即数学态射。现在,如果我们有一个名为IO的monad,它的行为如下:我们将这种类型的Monad称为IO,并在IO中处理打印(副作用)。大家可以结合我们之前学过的类型来举个例子:通常一个程序会有一个主入口函数main,这个主函数的返回值类型是IO。我们的sideeffects现在都运行在IO的范畴下,其他的操作可以保持纯(typeoperations)。IO类型允许我们处理Monad空间中那些烦人的副作用。这种Monad类型与Promise非常相似(对Promise域处理、可链接调用以及折叠和映射的有限副作用)。4.函数式编程的应用除了我们上面提到的一些例子,函数式编程还可以应用到更广泛的业务代码开发中,来替代我们的一些基础业务代码。这里有一些例子。4.1设计一个请求模块这种方式构建的模块具有很强的组合性和复用性。您还可以使用其他lodash库对req.js进行其他修改。我们在调用业务代码的时候,只需要传入params,分支校验和错误校验就可以交给validate.js中的高阶函数了。4.2设计一个输入框这个例子也是来源于常见的前端场景。我们利用函数式编程的思想,将多个看似无关的函数组合起来,得到业务需要的订阅函数,但同时,上述任意一个函数都可以用于其他的函数组合。比如回调函数可以直接回调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优点除了上述风格和特点,函数式编程相对于其他编程范式还有很多优点:函数纯粹,程序状态少,编码的精神负担更小。随着状态量的增加,一些编程范式构建的软件库的代码复杂度可能呈几何级数增长,而函数式编程的状态量趋于收敛,对软件复杂度的影响较小。引用透明允许你升级一个特定的功能而不影响其他功能(如果一个对象的引用需要改变,它可能会影响到整个身体)。高度可组合的函数易于重用(需要关注的状态更少),函数升级和改造更容易(高阶组件)。隔离副作用所有的状态量都汇聚到一个盒子(函数)中进行处理,关注点更加集中。代码简洁/流程更清晰一般来说,函数式编程风格的程序比其他编程风格的程序代码要少得多,这得益于函数的高度可组合性和大量完善的基础功能。简单性还使代码更易于维护。语义每个小功能完成一个小功能。当需要组合上层能力时,基本上可以根据函数语义快速组合。LazyComputationComposable函数只生成一个高阶函数,数据在函数最终被调用时在函数之间流动。跨语言统一性不同的语言似乎都遵循相似的函数式编程范式,比如Java8的lambda表达式、Rust的集合、匿名函数;虽然面向对象的实现在不同的语言中可能有很大差异,但函数式编程的统一性允许您轻松地跨语言开发。关键领域的应用由于函数式编程状态少、代码简洁的特点,在交互复杂、安全性要求高的领域有重要的应用。和Lisp、Haskell一样,都是因为上一波人工智能的浪潮才火起来的。在一些特殊领域(银行、水利、航天等)也得到了大规模应用。...6.2缺点当然,函数式编程也有一些缺点:学习曲线陡峭。面向对象和命令式编程范式都非常接近我们的日常习惯,而函数式编程则更加抽象。要想管理好副作用,可能需要学习很多新概念(reactive、monad等),这些概念入门难度大,是一个长期积累的过程。可能的调用栈溢出问题惰性计算在某些计算机或特殊的程序架构上可能会出现函数调用栈错误(调用链超长,递归超长),而且很多函数式编程语言需要编译器支持尾递归优化(优化成loops迭代)以获得更好的性能。额外的抽象负担当程序有大量的可变状态和副作用时,使用函数式编程可能会造成额外的抽象负担,项目开发周期可能会延长。这时候使用其他的抽象方法(比如OOP)可能会更好。数据不可变性问题为了保持数据不变,运行时可能会构建和生成大量的数据副本,导致更大的时间和空间消耗,降低性能;同时,数据不可变性可能会导致构建一些基本数据结构时语法不明显,性能变差(如LinkedList、HashMap等数据结构)。语义问题往往会为了开发一个功能而创建很多基本功能。如果大量的业务组件想要语义化命名,也会给开发者带来很大的负担;而且功能抽象能力因人而异,公共功能往往不够公共。或者过度设计。生态问题函数式编程因为抽象和性能问题,在工业生产领域一直被很多开发者所排斥,针对一些特定功能的解决方案也不太受欢迎(相对于其他编程范式),生态也一直比较小,这又是一些新开发者学习和使用函数式编程的又一个巨大障碍。...在日常的业务发展中,我们往往需要取长补短,在合适的领域使用合适的方法/范式。请记住,软件开发没有“银弹”。7.FAQQ:你认为Promise是MonadIO模型吗?答:我想是的。纯函数没有异步的概念,Promise使用了一个很好的方式将异步和IO转换为.then函数。你仍然可以在.then函数中编写纯函数,你可以在.then函数中调用其他promise,这与IOMonad的行为非常相似。Q:你愿意在生产中使用Haskell/Lisp/Clojure等纯函数式语言吗?A:不管你想不想用,现在很多语言都在引入函数式编程语法。并不是说函数式编程就一定好,但至少没那么可怕。有一点是肯定的,学习函数式编程可以拓展我们的思维,增加我们看问题的视角。问:有什么可预见的好处吗?答:是的。比如你在写代码的时候强制关注状态量(有多少,是否引用值,是否改变等),可以帮助你在写代码的时候减少状态量的使用,慢慢的复合一些状态量。,编写更简洁的代码。Q:函数式编程能给业务带来什么好处?A:业务拆分时,职能思维是单向的。我们会通过实现来思考需求对应的基本组件,递归思考。功能实现从最小粒度开始,上层通过功能组合逐步实现。与面向对象相比,这种方式组合起来更方便简洁,也更容易降低复杂度。比如面向对象中可能对象之间的相互引用和调用是没有限制的。这种模式带来思维逻辑的时候,思维就会发散。当业务复杂时,这种反差更加明显。面向对象设计需要优秀的设计模式来实现控制代码的复杂度不会增长得那么快。大多数情况下,函数式编程是一种单向数据流+基础工具库。降低了很多复杂性,并且生成的代码更清晰。8.作者简介俊杰,美团到家研发平台/医疗技术部前端工程师。9.参考资料维基百科:函数式编程/lambda演算/范畴论/集合论/群论。Github:getify/Functional-Light-JS《Learn You A Haskell For Great Good!》《Deep JavaScript》其他:各种网络博客阅读更多美团技术团队的技术文章前端|算法|后端|资料|安全|运维|iOS|安卓|测试|菜单栏对话框回复【2021货】、【2020货】、【2019货】、【2018货】、【2017货】等关键词,可以查看技术文章合集多年来美团技术团队的经验。|本文由美团技术团队制作,版权归美团所有。欢迎转载或将本文内容用于分享、交流等非商业用途,转载请注明“内容由美团技术团队转载”。本文未经许可不得转载或用于商业用途。任何商业活动,请发邮件至tech@meituan.com申请授权。