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

前言

1. 先览:代码组合和复用

在前端代码中,我们现有一些可行的模块复用方式,比如:

图 1

除了上面提到的组件和功能级别的代码复用,我们也可以在软件架构层面上,通过选择一些合理的架构设计来减少重复开发的工作量,比如说很多公司在中后台场景中大量使用的低代码平台

可以说,在大部分软件项目中,我们都要去探索代码组合和复用

函数式编程,曾经有过一段黄金时代,后来又因面向对象范式的崛起而逐步变为小众范式。但是,函数式编程目前又开始在不同的语言中流行起来了,像Java 8、JS、Rust等语言都有对函数式编程的支持。

今天我们就来探讨JavaScript的函数,并进一步探讨JavaScript中的函数式编程(关于函数式编程风格软件的组织、组合和复用)。

图 2

2. 什么是函数式编程?

2.1 定义

函数式编程是一种风格范式,没有一个标准的教条式定义。我们来看一下维基百科的定义:

函数式编程是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算是该语言最重要的基础。而且λ演算的函数可以接受函数作为输入的参数和输出的返回值。

我们可以直接读出以下信息:

  1. 避免状态变更
  2. 函数作为输入输出
  3. λ演算有关

那什么是λ演算呢?

2.2 函数式编程起源:λ演算

λ演算(读作lambda演算)由数学家阿隆佐·邱奇在20世纪30年代首次发表,它从数理逻辑(Mathematical logic)中发展而来,使用变量绑定(binding)和代换规则(substitution)来研究函数如何抽象化定义(define)、函数如何被应用(apply)以及递归(recursion)的形式系统。

λ演算和图灵机等价(图灵完备,作为一种研究语言又很方便)。

通常用这个定义形式来表示一个λ演算

图 3

所以λ演算式就三个要点:

  1. 绑定关系。变量任意性,x、y和z都行,它仅仅是具体数据的代称。
  2. 递归定义。λ项递归定义,M可以是一个λ项。
  3. 替换归约。λ项可应用,空格分隔表示对M应用NN可以是一个λ项。

比如这样的演算式:

图 4

通过变量代换(substitution)归约(reduction),我们可以像化简方程一样处理我们的演算。

λ演算有很多方式进行,数学家们也总结了许多和它相关的规律和定律(可查看维基百科)。

举个例子,小时候我们学习整数就是学会几个数字,然后用加法/减法来推演其他数字。在函数式编程中,我们可以用函数来定义自然数,有很多定义方式,这里我们讲一种实现方式:

图 5

上面的演算式表示有一个函数f和一个参数x。令0x1f x2f 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替换m3替换n,那么整个表达式可以化简为4。这也是函数式编程里面的引用透明性的由来。需要注意的是,这里的13表示表达式运算值,可以替换为其他表达式。比如把**1替换为(λm.λn.m + n 1 3)**,这里就需要做两次归约来得到下面的最终结果:

图 11

2.3 JavaScript中的λ表达式:箭头函数

ECMAScript 2015规范引入了箭头函数,它没有this,没有arguments。只能作为一个表达式(expression)而不能作为一个声明式(statement),表达式产生一个箭头函数引用,该箭头函数引用仍然有namelength属性,分别表示箭头函数的名字、形参(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

我们把λ演算中fx分别取为countTimex,代入运算就得到了我们的自然数。

这也说明了不管你使用符 系统还是JavaScript语言,你想要表达的自然数是等价的。这也侧面说明λ演算是一种形式上的抽象(和具体语言表述无关的抽象表达)

2.4 函数式编程基础:函数的元、柯里化和Point-Free

回到JavaScript本身,我们要探究函数本身能不能带给我们更多的东西?我们在JavaScript中有很多创建函数的方式:

图 16

虽然函数有这么多定义,但function关键字声明的函数带有arguments和this关键字,这让他们看起来更像是对象方法(method),而不是函数(function) 。

况且function定义的函数大多数还能被构造(比如new Array)。

接下来我们将只研究箭头函数,因为它更像是数学意义上的函数(仅执行计算过程)。

  • 没有arguments和this。
  • 不可以被构造new。
  • 2.4.1 函数的元:完全调用和不完全调用

    不论使用何种方式去构造一个函数,这个函数都有两个固定的信息可以获取:

  • name 表示当前标识符指向的函数的名字。
  • length 表示当前标识符指向的函数定义时的参数列表长度。
  • 在数学上,我们定义f(x) = x是一个一元函数,而f(x, y) = x + y是一个二元函数。在JavaScript中我们可以使用函数定义时的length来定义它的元。

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2022年9月28日
    下一篇 2022年9月28日

    相关推荐