本文首发于 vivo互联网技术 微信公众号
连接:mp.weixin.qq.com/s/EWSqZuujH…
做者:杨昆
前端
【编写高质量函数系列】中,react
《如何编写高质量的 JS 函数(1) -- 敲山震虎篇》介绍了函数的执行机制,此篇将会从函数的命名、注释和鲁棒性方面,阐述如何经过 JavaScript 编写高质量的函数。程序员
《如何编写高质量的 JS 函数(2)-- 命名/注释/鲁棒篇》从函数的命名、注释和鲁棒性方面,阐述如何经过 JavaScript编写高质量的函数。
编程
这是编写高质量函数系列文章的函数式编程篇。咱们来讲一说,如何运用函数式编程来提升你的函数质量。
api
函数式编程篇分为两篇,分别是理论篇和实战篇。此篇文章属于理论篇,在本文中,我将经过背景加提问的方式,对函数式编程的本质、目的、前因后果等方面进行一次清晰的阐述。
浏览器
写做逻辑缓存
经过对计算机和编程语言发展史的阐述,找到函数式编程的时代背景。经过对与函数式编程强相关的人物介绍,来探寻和感觉函数式编程的那些鲜为人知的本质。
bash
下面列一个简要目录:
微信
计算机和编程语言的发展史数据结构
为何会有函数式语言?函数式语言是如何产生的?它存在的意义是什么?
lambda 演算系统是啥?lambda 具体说的是啥内容?lambda 和函数有啥联系?为啥会有 lambda 演算系统?
函数式编程为何要用函数去实现?
函数式语言中,或者在函数式编程中,函数二字的含义是什么?它具有什么能力?
函数式编程的特性关键词有哪些?
命令式和函数式编程是对立的吗?
按照 FP 思想,不能使用循环,那咱们该如何去解决?
抛出异常会产生反作用,但若是不抛出异常,又该用什么替代呢?
函数式编程不容许使用可变状态的吗?如何没有反作用的表达咱们的程序?
为何函数式编程建议消灭掉语句?
为何函数式编程要避免使用 this
JavaScript 中函数是一等公民, 就能够得出 JavaScript 是函数式语言吗?为何说 JS 是多态语言?
为何 JS 函数内部可使用 for 循环吗?
JS 函数是一等公民是啥意识?这样作的目的是啥?
用 JS 进行函数式编程的缺点是什么?
函数式编程的将来。
简要目录介绍完啦,你们请和我一块儿往下看。
PS:我好像是一个在海边玩耍的孩子,不时为拾到比一般更光滑的石子,或更美丽的贝壳而欢欣鼓舞,而展示在我面前的是彻底未探明的的真理之海。
计算机和编程语言的发展史是由人类主导的,去了解在这个过程当中起到关键做用的人物是很是重要的。
下面咱们一块儿来认识几位起关键做用的超巨。
希尔伯特被称为数学界的无冕之王 ,他是天才中的天才。
在我看来,希尔伯特最厉害的一点就是:他鼓舞你们去将证实过程纯机械化,由于这样,机器就能够经过形式语言推理出大量定理。
也正是他的坚持推进,形式语言才逐渐走向历史的舞台中央。
艾伦·麦席森·图灵被称为计算机科学之父。
我认为,他最伟大的成就,就是发明了图灵机:
上图所示,就是图灵机的模型图。
这里咱们注意一点:从图中,咱们会发现,每一个小方格可存储一个数字或者字母。这个信息很是重要,你们能够思考一下。
PS: 等我介绍 冯·诺依曼 的时候,就会明白它们之间的联系。
阿隆佐·邱奇,艾伦·麦席森·图灵的博导。
他最伟大的成就,就是:发明了 λ(lambda) 演算。
如上图,就是 λ(lambda) 演算的基本形式。
阿隆佐·邱奇发明的 λ演算和图灵发明的图灵机,一块儿改写了当今世界,形式语言的历史。
思考: 邱奇的 λ演算 和图灵的图灵机,这二者有什么区别和联系?
冯·诺依曼被称为计算机之父。
他提出了冯·诺依曼体系结构:
从上图,咱们能够看出:冯·诺依曼体系结构由运算器、控制器、存储器、输入设备、输出设备五个部分组分组成。采用二进制逻辑,程序存储、执行做为计算机制造的三个原则。
注意一个信息:咱们知道,计算机底层指令都是由 0 和 1 组成,经过对 0 和 1 的 CRUD ,来完成各类计算操做。咱们再看图灵机,会发现其每一个小方格可存储一个数字或者字母。
看到这,是否是发现冯·诺依曼体系结构和图灵机有一些联系。
是的,现冯·诺依曼体系结构就是按照图灵机的模型来实现的计算机结构。计算机的 0 和 1 ,就是图灵机的小方格中的数字或者字母的特例。
由于若是想完全解开函数式编程的困惑,那就必需要去了解这时代背景和关键人物的一些事迹。
邱奇是图灵的博士生导师,他们之间有一个著名的论题,那就是 邱奇-图灵论题 。
论题大体的内容是:图灵和 lambda 这两种模型,有没有一个模型能表示的计算,另外一个模型表示不了呢?
到目前为止,这个论题尚未答案。也正由于如此,让不少人对 lambda 模型充满了信心。后面的岁月中,lambda 模型一直在被不少人研究、论证、实践。
它叫 ENAIC。
1946 年,世界上第一台电子计算机—— ENIAC 问世,它能够改变计算方式,便可以更改程序。
也就是说:它是一台可编程计算机。
perl 语言的设计者 Larry Wall 说过:优秀的程序员具备三大美德:懒惰、急躁、傲慢。
可编程完美诠释了懒惰的美德。在 ENAIC 诞生后,出现了各类各样的 程序设计语言。三大美德也提现的淋漓尽致。
上图能够得到如下信息:
程序设计语言只是计算机语言的一个分类。
HTML 、XML 是数据设计语言。
在程序设计语言中,分为说明式和声明式。
在说明式中,又包含函数式、逻辑式等。其实 MySQL,就是逻辑式语言,它经过提问的方式来完成操做。
冯诺依曼体系更符合面向过程的语言。
这个分类能够好好看看,会有一些感觉的。
上图很是简单明了,直到 1995 年。
时间线大概是这样的:xxx ---> xxx ---> .... ---> JavaScript ...
时间来到了 1996 年,JavaScript 诞生了!
图中这位老哥叫布兰登·艾奇 。那一年,他34岁。
从上图中你会有以下几点感觉:
第一个感觉:阿布对 Java 一点兴趣也没有。
第二个感觉:因为讨厌 Java ,阿布不想用 Java 的对象表示形式,因而就借鉴了 Self 语言,使用基于原型的继承机制。埋下了前几年前端界用原型进行面对对象编程的种子。
第三个感觉:阿布借鉴了 Scheme 语言,将函数提高到一等公民的地位,让 JS 拥有了函数式编程的能力。埋下了 JS 能够进行函数式编程的种子。
第四个感觉:JS 是既能够函数式编程,也能够面对对象编程。
我在回顾程序设计语言的发展史和一些故过后,我并不认为 JavaScript 是一个烂语言,相反正是这种中庸之道,才使得 JavaScript 可以流行到如今。
经过对计算机语言的发展史和关键人物的简洁介绍,咱们能够从高层面去体会到函数式编程在计算机语言发展史中的潜力和影响力。
不过,经过背景和人物的介绍,对函数式编程的理解仍是有限的。下面我将经过提问的方式来阐述函数式编程的前因后果。
下面将经过 10 个问题的解答,来阐述函数式编程的理论支撑、函数式编程的诞生背景、函数式编程的核心理论以及推导等知识。
函数式语言的存在,是为了实现运算系统的本质——运算。
计算机未问世以前,四位大佬 阿兰·图灵、约翰 ·冯·诺依曼 、库尔特 ·哥德尔 和阿隆左 ·丘奇。展开了对形式化的运算系统的研究。
经过形式系统来证实一个命题:能够用简单的数学法则表达现实系统。
从上文的图片和分析可知,图灵机和冯诺依曼体系的计算机系统都依赖存储(内存)进行运算。
换句话说就是:经过修改内存来反映运算的结果。并非真正意义上的运算。
修改内存并非咱们想要的,咱们想要的仅仅是运算。从目的性的角度看,修改内存能够说是运算系统中的反作用。或者说,是表现运算结果的一种手段。
这一切,图灵的博导邱奇看在眼里,他看到了问题的本质。为了实现运算系统的本质——运算,即不修改内存,直接经过运算拿到结果。
他提出了 lambda 演算的形式系统,一种更接近于运算才是本质的理论。
从语言学分类来讲:是两种不一样类型的计算范型。
从硬件系统来讲:它们依赖于各自不一样的计算机系统(也就是硬件)。为何依赖不一样的硬件,是由于若是用冯诺依曼结构的计算机,就意味着要靠修改内存来实现运算。可是,这和 lambda 演算系统是相矛盾的。
由于基于 lambda 演算系统实现的函数式语言,是不须要寄存器的,也不存在须要使用寄存器去存储变量的状态。它只注重运算,运算结束,结果就会出来。
最大的隔阂就是依赖各自不一样的计算机系统 。
目前为止,在技术上作不到基于 A 范型的计算机系统,同时支持 B 范型。也就是说,不能期望在 X86 指令集中出现适用于 lambda 演算 的指令、逻辑或者物理设计。
你可能会疑问,既然硬件不支持,那咱们为何还能进行函数式编程?
其实现实中,大多数人都是用的冯诺依曼体系的命令式语言。因此为了得到特别的计算能力和编程特性。语言就在逻辑层虚拟一个环境,也由于这样,诞生了 JS 这样的多范型语言,以及 PY 这种脚本语言。
究其根源,是由于,冯·诺依曼体系的计算机系统是基于存储与指令系统的,并非基于运算的。
在当时硬件设备条件的限制下,邱奇提出的 lambda 演算,在很长时间内,都没有被程序设计语言所实现。
直到冯诺依曼等人完成了 EDVAC 的十年以后。一位 MIT 的教授 John McCarthy 对邱奇的工做产生了兴趣。在 1958 年,他公开了表处理语言 LISP 。这个 LISP 语言就是对邱奇的 lambda 演算的实现。
自此,世界上第一个函数式语言诞生了。
LISP 就是函数式语言的鼻祖,完成了 lamda 演算的实现,实现了 运算才是本质的运算系统。
上图是 Lisp 的图片,感觉一下图片符号的魅力。
为何我说是曙光?
是由于,并无真正的胜利。此时的 LISP 依旧是工做在冯·诺依曼计算机上,由于当时只有这样的计算机系统。
因此从 LISP 开始,函数式语言就是运行在解释环境而非编译环境中的。也就是传说中的脚本语言,解释器语言。
直到 1973 年,MIT 人工智能实验室的一组程序员开发了,被称为 LISP 机器的硬件。自此,阿隆左·丘奇的 lambda 演算终于获得了 硬件实现。终于有一个计算机(硬件)系统能够宣称在机器指令级别上支持了函数式语言。
关于这问,我阐述了不少,从函数式语言诞生的目的、到函数式语言诞生的艰难过程、再到计算机硬件的限制。最后在不断的努力下,作到了既能够经过解释器,完成基于冯·诺依曼体系下,计算机系统的函数式编程。也能够在机器指令级别上支持了函数式语言的计算机上进行纯正的函数式编程。
思考题:想想,在现在,函数式编程为何愈来愈被人所了解和掌握。
lambda 是一种解决数学中的函数语义不清晰,很难表达清楚函数的结构层次的问题的运算方案。
也就是在运算过程当中,不使用函数中的函数运算形式,而使用 lambda 的运算形式来进行运算。
(1)一套用于研究函数定义、函数应用和递归的系统。
(2)函数式语言就是基于 lambda 运算而产生的运算范型。
lambda 演算系统是学习函数式编程的一个很是重要的知识点。它是整个函数式编程的理论基石。
以下图所示:
从上面的数学函数中,咱们能够发现如下几点:
没有显示给出函数的自变量
对定义和调用区分不严格。x2-2*x+1 既能够当作是函数 f(x) 的定义,又能够当作是函数 g(x) 对变量 x-1 的调用。
体会上面几点,咱们会发现:数学中的函数语义并不清晰,它很难表达清楚函数的结构层次。对此,邱奇给出了解决方法,他提出了 lambda(λ) 演算。
基本定义形式:λ<变量>.<表达式>
经过这种方法定义的函数就叫 λ(lambda) 表达式。
咱们能够把 lambda 翻译成函数,便可以把 lambda 表达式念成函数表达式。
PS: 这里说一下,函数式语言中的函数,是指 lambda(函数),它和咱们如今的通用语言中,好比 C 中 的 function 是不一样的两个东西。
(λx.x2-2*x+1)1
应用(也就是调用)过程,就是把变量值赋值给表达式中的 x ,并去掉 λ <变量>,过程以下:
(λx.x2-2*x+1)1=1-2*1+1=0
表达式 λx.λy.x+y 中,有两个变量 分别为 x 和 y。
当 x=1, y=2 表达式调用过程以下:
((λx.λy.2*x+y)1)2 = (λy.2+y) 2 = 4
从上面,咱们能够看到,lambda 表达式的调用中,参数是有执行顺序的,能感觉到柯里化和组合的味道。
也就是说,因为函数就是表达式,表达式就是值。因此函数的返回值能够是一个函数,而后继续进行调用执行,循环往复。
这样,不一样函数的层次问题也解决了,这里用到了高阶函数。在函数式编程语言中,当函数是一等公民时,这个规律是生效的。
说到这,你们从根本上对函数式编程有了一个清晰的认知。好比它的数学基础,为何存在、以及它和命令式语言的本质不一样点。
lambda 演算系统 证实了:任何一个可计算函数都能用这种形式来表达和求值,它等价于图灵机。
至此,我阐述了函数式语言出现的缘由。以及支持函数式语言的重要理论支撑 —— lambda 演算系统的由来和基本内容。
上文提到过,运算系统的本质是运算。
函数只是封装运算的一种手段,函数并非真正的精髓,真正的精髓在于运算。
说到这,你们从根本上对函数式编程有了一个清晰的认知。好比它的数学基础,为何存在、以及它和命令式语言的本质不一样点。
这个函数是特指 知足 lambda 演算的 lambda 表达式。函数式编程中的函数表达式,又称为 lambda 表达式。
该函数具备四个能力:
能够调用
是运算元
能够在函数内保存数据
函数内的运算对函数外无反作用
在 JS 中,函数也是运算元,但它的运算只有调用。
闭包的存在使得函数内保存数据获得了实现。函数执行,数据存在不一样的闭包中,不会产生相互影响,就像面对对象中不一样的实例拥有各自的自私有数据。多个实例之间不存在可共享的类成员。
从这问能够知道,并非一个语言支持函数,这个语言就能够叫作函数式语言,或者说就具备函数式编程能力。
大体列一下:
引用透明性、纯洁性、无反作用、幂等性、惰性求值/非惰性求值、组合、柯里化、管道、高阶性、闭包、不可变性、递归、partial monad 、 monadic 、 functor 、 applicative 、尾递归、严格求值/非严格求值、无限流和共递归、状态转移、 pointfree 、一等公民、隐式编程/显式编程等。
定义:任何程序中符合引用透明的表达式均可以由它的结果所取代,而不改变该程序的含义。
意义:让代码具备获得更好的推导性、能够直接转成结果。
举个例子:好比将 TS 转换成 JS 的过程当中,若是表达式具有引用透明性。那么在编译的时候,就能够提早把表达式的结果算出来,而后直接变成值,在 JS 运行的时候,执行的时间就会下降。
定义:对于相同的输入都将返回相同的输出。
优势:
可测试
无反作用
能够并行代码
能够缓存
定义:若是一个参数是须要用到时,才会完成求值(或取值) ,那么它就是惰性求值的。反之,就是非惰性求值。
(1)惰性求值:
true || console.log('源码终结者')复制代码
特色:当再也不须要后续表达式的结果的时候,就终止后续的表达式执行,提升了速度,节约了资源。
(2)非惰性求值:
let i = 200
console.log(i+=20, i*=2, 'value: ' + i)
console.log(i)复制代码
特色:浪费 cpu 资源,会存在不肯定性。
函数无须要说起将要操做的数据是什么。也就是说,函数不用指明操做的参数,而是让组合它的函数来处理参数。
一般使用柯里和组合来实现 pointfree。
(1)没有组合的状况:
(2)组合后的状况:
具体的看我后面的实战篇,我会经过例子来介绍组合的做用。
图片:Typescript版图解Functor , Applicative 和 Monad
这些高级知识点,随便一个都够解释很长的,这里我就不作解释了。我推荐一篇文章,阐述的很是透彻。
对于这三个高级知识点,我有些我的的见解。
第一个:不要被名词吓到,经过敲代码去感觉其差别性。
第二个:既然要去理解函数式语言的高级知识,那就要尽量的摆脱命令式语言的固有思想,而后再去理解这些高级知识点。
第三个:为何函数式编程中,会有这些高级知识点?
关于第三个见解,我我的的感觉就是:函数式编程,须要你将隐式编程风格改为显式风格。这也就意味着,你要花不少时间在函数的输入和输出上。
如何解决这个问题?
能够经过上述的高级知识点来完成,在特定的场景下,好比在 IO 中,不须要列出全部的可能性,只须要经过一个抽象过程来完成全部状况的处理,并保证不会抛出异常。
它们都是为了一个目的,减小重复代码量,提升代码复用性。
此问,我没有详细回答。我想说的是:
这些特性关键词,都值得认真研究,这里我只介绍了我认为该注意的点,具体的知识点,你们自行去了解和研究。
从前面提到的一些阐述来看,命令式编程和函数式编程不是对立的。它们既能够独立存在,又能够共生。而且在共生的状况下,会发挥出更大的影响力。
我我的认为,在编程领域中,多范式语言才是王道,单纯只支持某一种范式的编程语言是没法适应多场景的。
对于纯函数式语言,没法使用循环。咱们能想到的,就是使用递归来实现循环,回顾一下前面提到的 lamda 演算系统,它是一套用于研究函数定义、函数应用和递归的系统。因此做为函数式语言,它已经作好了使用递归去完成一切循环操做的准备了。
说到这,咱们须要转变一下观念:好比在命令式语言中,咱们一般都是使用 try catch 这种来捕获抛出的异常。可是在纯函数式语言中,是没有 try catch 的,一般使用函子来代替 try catch 。
看到上面这些话,你可能会感到不能理解,为何要用函子来代替 try catch 。
其实有困惑是很正常的,主要缘由就是:咱们站在了命令式语言的理论基石上去理解函数式语言。
若是咱们站在函数式语言的理论基石上去理解函数式语言,就不会感受到困惑了。你会发现只能用递归实现循环、没有 try catch 等要求,是合理且合适的。
PS: 这就好像是一直使用函数式语言的人忽然接触命令式语言,也会满头雾水的。
可使用局部的可变状态,只要该局部变量不会影响外部,那就能够说改函数总体是没有反作用的。
由于语句的本质是:在于描述表达式求值的逻辑,或者辅助表达式求值。
主要有如下两点缘由:
JS 的 this 有多种含义,使用场景复杂。
this 不取决于函数体内的代码。
全部的数据都应以参数的形式提供给函数,而 this 不遵照这种规则。
不少人可能没有想过这个问题
其实在纯函数式语言中,是不存在循环语句的。循环语句须要使用递归实现,可是 JS 的递归性能并很差,好比没有尾递归优化,那怎么办呢?
为了能支持函数式编程,又要避免 JS 的递归性能问题。最后容许了函数内部可使用 for 循环,你会看到 forEach 、 map 、 filter 、 reduce 的实现,都是对 for 循环进行了封装。内部仍是使用了 for 循环。
PS: 在 JS 中,只要函数内的 for 循环不影响外部,那就能够当作是体现了纯洁性。
我总结了一下,大概有如下意识:
可以表达为匿名的直接量
能被变量存储
能被其它数据结构存储
有独立而肯定的名称(如语法关键字)
可比较的
可做为参数传递
可做为函数结果值返回
在运行期可建立
可以以序列化的形式表达
可(以天然语言的形式)读的
可(以天然语言能在分布的或运行中的进程中传递与存储形式)读的
这个是什么意识呢?
在 js 中,咱们会发现有 eval 这个 api 。正是由于可以支持以序列化的形式表达,才能作到经过 eval 来执行字符串形式的函数。
JS 之父设计函数为一等公民的初衷就是想让 JS 语言能够支持函数式编程。
函数是一等公民,就意味着函数能作值能够作的任何事情。
核心思想:经过表达式消灭掉语句。
有如下几个路径:
经过表达式消灭分支语句 举例:单个 if 语句,能够经过布尔表达式消灭掉
经过函数递归消灭循环语句
用函数去代替值(函数只有返回的值在影响系统的运算,一个函数调用过程其实只至关于表达式运算中的一个求值)
缺乏不可变数据结构( JS 除了原始类型,其余都是可变的)
没有提供一个原生的利于组合函数而产生新函数的方式,须要第三方支持
不支持惰性序列
缺乏尾递归优化
JS 的函数不是真正纯种函数式语言中的函数形式(好比 JS 函数中能够写循环语句)
表达式支持赋值
对于函数式编程来讲,缺乏尾递归优化,是很是致命的。就目前而言,浏览器对尾递归优化的支持还不是很好。
什么是尾递归?
以下图所示:
咱们来看下面两张图:
第一张图,没有使用尾递归,由于 n * fatorial(n - 1) 是最后一个表达式,而 fatorial(n - 1) 不是最后一个表达式。第二张图,使用了尾递归,最后一个表达式就是递归函数自己。
问题来了,为何说 JS 对尾递归支持的很差呢?
这里我想强调的一点是,全部的解释器语言,若是没有解释环境,也就是没有 runtime ,那么它就是一堆文本而已。JS 主要跑在浏览器中,须要浏览器提供解释环境。若是浏览器的解释环境对 JS 的尾递归优化的很差,那就说明,JS 的尾递归优化不好。因为浏览器有不少,可见 JS 要实现全面的尾递归优化,还有很长的路要走。
PS: 任何需求都是有优先级的,对浏览器来讲,像这种尾递归优化的优先级,明显不高。我我的认为,优先级不高,是到如今极少有浏览器支持尾递归优化的缘由。
符号: 抽象、语义
Typescript版图解Functor , Applicative 和 Monad
邱奇-图灵论题与lambda演算
为何须要Monad?
为何是Y?
JavaScript 函数式编程指南
Scala 函数式编程
Haskell 趣学指南
其余电子书
本文经过阐述加提问的方式,对函数式编程的一些理论知识进行了一次较为清晰的阐述。限于篇幅,一些细节没法展开,若有疑问,能够与我联系,一块儿交流一下,共同进步。
如今的前端,依旧在快速的发展中。从最近的 react Hooks 到 Vue 3.0 的 Function API 。咱们能感觉到,函数式编程的影响力在慢慢变大。
在可见的将来,函数式编程方面的知识,在脑海里,是要有一个清晰的认知框架。
最后,发表下我我的的见解:
JavaScript 最终会回到以函数的形式去处理绝大多数事情的模式上。
更多内容敬请关注 vivo 互联网技术 微信公众号
注:转载文章请先与微信号:labs2020 联系。