前言
1. 先览:代码组合和复用
在前端代码中,我们现有一些可行的模块复用方式,比如:
图 1
除了上面提到的组件和功能级别的代码复用,我们也可以在软件架构层面上,通过选择一些合理的架构设计来减少重复开发的工作量,比如说很多公司在中后台场景中大量使用的低代码平台。
可以说,在大部分软件项目中,我们都要去探索代码组合和复用。
函数式编程,曾经有过一段黄金时代,后来又因面向对象范式的崛起而逐步变为小众范式。但是,函数式编程目前又开始在不同的语言中流行起来了,像Java 8、JS、Rust等语言都有对函数式编程的支持。
今天我们就来探讨JavaScript的函数,并进一步探讨JavaScript中的函数式编程(关于函数式编程风格软件的组织、组合和复用)。
图 2
2. 什么是函数式编程?
2.1 定义
函数式编程是一种风格范式,没有一个标准的教条式定义。我们来看一下维基百科的定义:
函数式编程是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算是该语言最重要的基础。而且λ演算的函数可以接受函数作为输入的参数和输出的返回值。
我们可以直接读出以下信息:
- 避免状态变更
- 函数作为输入输出
- 和λ演算有关
那什么是λ演算呢?
2.2 函数式编程起源:λ演算
λ演算(读作lambda演算)由数学家阿隆佐·邱奇在20世纪30年代首次发表,它从数理逻辑(Mathematical logic)中发展而来,使用变量绑定(binding)和代换规则(substitution)来研究函数如何抽象化定义(define)、函数如何被应用(apply)以及递归(recursion)的形式系统。
λ演算和图灵机等价(图灵完备,作为一种研究语言又很方便)。
通常用这个定义形式来表示一个λ演算。
图 3
所以λ演算式就三个要点:
- 绑定关系。变量任意性,x、y和z都行,它仅仅是具体数据的代称。
- 递归定义。λ项递归定义,M可以是一个λ项。
- 替换归约。λ项可应用,空格分隔表示对M应用N,N可以是一个λ项。
比如这样的演算式:
图 4
通过变量代换(substitution)和归约(reduction),我们可以像化简方程一样处理我们的演算。
λ演算有很多方式进行,数学家们也总结了许多和它相关的规律和定律(可查看维基百科)。
举个例子,小时候我们学习整数就是学会几个数字,然后用加法/减法来推演其他数字。在函数式编程中,我们可以用函数来定义自然数,有很多定义方式,这里我们讲一种实现方式:
图 5
上面的演算式表示有一个函数f和一个参数x。令0为x,1为f x,2为f f x…
什么意思呢?这是不是很像我们数学中的幂:a^x(a的x次幂表示a对自身乘x次)。相应的,我们理解上面的演算式就是数字n就是f对x作用的次数。有了这个数字的定义之后,我们就可以在这个基础上定义运算。
图 6
其中SUCC表示后继函数(**+1操作),PLUS**表示加法。现在我们来推导这个正确性。
图 7
这样,进行λ演算就像是方程的代换和化简,在一个已知前提(公理,比如0/1,加法)下,进行规则推演。
2.2.1 演算:变量的含义
在λ演算中我们的表达式只有一个参数,那它怎么实现两个数字的二元操作呢?比如加法a + b,需要两个参数。
这时,我们要把函数本身也视为值,可以通过把一个变量绑定到上下文,然后返回一个新的函数,来实现数据(或者说是状态)的保存和传递,被绑定的变量可以在需要实际使用的时候从上下文中引用到。
比如下面这个简单的演算式:
图 8
第一次函数调用传入m=5,返回一个新函数,这个新函数接收一个参数n,并返回m + n的结果。像这种情况产生的上下文,就是Closure(闭包,函数式编程常用的状态保存和引用手段),我们称变量m是被绑定(binding)到了第二个函数的上下文。
除了绑定的变量,λ演算也支持自由的变量,比如下面这个y:
图 9
这里的y是一个没有绑定到参数位置的变量,被称为一个自由变量。
2.2.2 演算:代换和归约
演算分为Alpha代换和Beta归约。 前面章节我们实际上已经涉及这两个概念,下面来介绍一下它们。
Alpha代换指的是变量的名称是不重要的,你可以写λm.λn.m + n,也可以写λx.λy.x + y,在演算过程中它们表示同一个函数。也就是说我们只关心计算的形式,而不关心细节用什么变量去实现。这方便我们不改变运算结果的前提下去修改变量命名,以方便在函数比较复杂的情况下进行化简操作。实际上,连整个lambda演算式的名字也是不重要的,我们只需要这种形式的计算,而不在乎这个形式的命名。
Beta归约指的是如果你有一个函数应用(函数调用),那么你可以对这个函数体中与标识符对应的部分做代换(substitution),方式为使用参数(可能是另一个演算式)去替换标识符。听起来有点绕口,但是它实际上就是函数调用的参数替换。比如:
图 10
可以使用1替换m,3替换n,那么整个表达式可以化简为4。这也是函数式编程里面的引用透明性的由来。需要注意的是,这里的1和3表示表达式运算值,可以替换为其他表达式。比如把**1替换为(λm.λn.m + n 1 3)**,这里就需要做两次归约来得到下面的最终结果:
图 11
2.3 JavaScript中的λ表达式:箭头函数
ECMAScript 2015规范引入了箭头函数,它没有this,没有arguments。只能作为一个表达式(expression)而不能作为一个声明式(statement),表达式产生一个箭头函数引用,该箭头函数引用仍然有name和length属性,分别表示箭头函数的名字、形参(parameters)长度。一个箭头函数就是一个单纯的运算式,箭头函数我们也可以称为lambda函数,它在书写形式上就像是一个λ演算式。
图 12
可以利用箭头函数做一些简单的运算,下例比较了四种箭头函数的使用方式:
图 13
这是直接针对数字(基本数据类型)的情况,如果是针对函数做运算(引用数据类型),事情就变得有趣起来了。我们看一下下面的示例:
图 14
fn_x类型,表明我们可以利用函数内的函数,当函数被当作数据传递的时候,就可以对函数进行应用(apply),生成更高阶的操作。 并且x => y => x(y)可以有两种理解,一种是x => y函数传入X => x(y),另一种是x传入y => x(y)。
add_x类型表明,一个运算式可以有很多不同的路径来实现。
上面的add_1/add_2/add_3我们用到了JavaScript的立即运算表达式IIFE。
λ演算是一种抽象的数学表达方式,我们不关心真实的运算情况,我们只关心这种运算形式。因此上一节的演算可以用JavaScript来模拟。下面我们来实现λ演算的JavaScript表示。
图 15
我们把λ演算中的f和x分别取为countTime和x,代入运算就得到了我们的自然数。
这也说明了不管你使用符 系统还是JavaScript语言,你想要表达的自然数是等价的。这也侧面说明λ演算是一种形式上的抽象(和具体语言表述无关的抽象表达)。
2.4 函数式编程基础:函数的元、柯里化和Point-Free
回到JavaScript本身,我们要探究函数本身能不能带给我们更多的东西?我们在JavaScript中有很多创建函数的方式:
图 16
虽然函数有这么多定义,但function关键字声明的函数带有arguments和this关键字,这让他们看起来更像是对象方法(method),而不是函数(function) 。
况且function定义的函数大多数还能被构造(比如new Array)。
接下来我们将只研究箭头函数,因为它更像是数学意义上的函数(仅执行计算过程)。
2.4.1 函数的元:完全调用和不完全调用
不论使用何种方式去构造一个函数,这个函数都有两个固定的信息可以获取:
在数学上,我们定义f(x) = x是一个一元函数,而f(x, y) = x + y是一个二元函数。在JavaScript中我们可以使用函数定义时的length来定义它的元。
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!