定义
"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。
它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。举例来说,现在有这样一个数学表达式:
(1 + 2) * 3 - 4
传统的过程式编程,可能这样写:
var a = 1 + 2; var b = a * 3; var c = b - 4;
函数式编程要求使用函数,我们可以把运算过程定义为不同的函数,然后写成下面这样:
var result = subtract(multiply(add(1,2), 3), 4);
优点
编程范式的意义在于它提供了模块化代码的各种思想和方法。函数式编程亦然。
- 代码简洁,开发快速
- 接近自然语言,易于理解
- 更方便的代码管理
- 易于"并发编程"
- 提供代码的热更新
函数式编程是以函数为核心来组织模块的一套编程方法。
函数是“一等公民”
是指函数和其他数据类型拥有平等的地位,可以赋值给变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。
它是函数式编程得以实现的前提,因为我们基本的操作都是在操作函数。这个特性意味着函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值,回调函数就是典型应用。
无状态和数据不可变
这是函数式编程的核心概念:
- 数据不可变: 它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
- 无状态: 主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。
高阶函数
高价函数接受一个或多个函数为参数,返回一个新的函数,两个条件满足其一则就是高阶函数,高阶函数用于修改函数的参数或者控制函数的执行流程与返回结果;常见的高阶函数有curry
、compose
、以及数组的一些方法map
、find
等;
function add(a, b){
return a + b
}
// HOF
function HOF(fun){
return function(...args){
return args[0] - args[1]
}
}
var transformAdd = HOF(add);
console.log(transformAdd(5, 2)) // 3
以上函数已经过高阶函数HOF
包装,硬生生的改变了函数的执行方式与结果;高价函数可以用来封装统一的函数mixin
,可以更细粒度的控制函数,更好的提现函数的“单一职责”思想;
纯函数
纯函数是指同时满足下面两个条件的函数:
- 函数的结果只依赖于输入的参数且与外部系统状态无关——只要输入相同,返回值总是不变的。
- 除了返回值外,不修改程序的外部状态(比如全局变量、入参)。——满足这个条件也被称作“没有副作用(副作用是指,函数内部与外部互动,产生运算以外的其他结果。 例如在函数调用的过程中,利用并修改到了外部的变量,那么就是一个有副作用的函数。)
纯函数是相对独立的程序构件。因为函数的结果只依赖于输入的参数且与外部系统状态无关,使得单元测试和debug变得异常容易,而这也正是模块化的优点之一。
引用透明
指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。
惰性执行
是指的是函数只在需要的时候执行,即不产生无意义的中间变量。函数式编程跟命令式编程最大的区别就在于几乎没有中间变量,它从头到尾都在写函数,只有在最后的时候才产生实际的结果。
柯里化和函数组合
柯里化
是指将一个多元函数,转换成一个依次调用的单元函数。
f(a,b,c) → f(a)(b)(c)
例如:
const add = (x) => { return (y, z) => { return x + y + z } }
let increase = add(1); console.log(increase(2, 3)); // 6
柯里化应用
在实践中使用柯里化都是为了把某个函数变得单值化,这样可以增加函数的多样性,使得其适用性更强:
const replace = curry((a, b, str) => str.replace(a, b));
const replaceSpaceWith = replace(/\s*/);
const replaceSpaceWithComma = replaceSpaceWith(',');
const replaceSpaceWithDash = replaceSpaceWith('-');
通过上面这种方式,我们从一个 replace 函数中产生很多新函数,可以在各种场合进行使用。
单值函数是我们即将讲到的函数组合的基础。
函数组合
是指将数个函数对象作为参数传递给一个函数,在该函数内部将传入的函数参数分别作为参数进行嵌套。
例如:
const compose = (f, g) => x => f(g(x))
const f = x => x + 1; const g = x => x * 2;
const fg = compose(f, g); fg(1) //3
我们可以看到 compose 就实现了一个简单的功能:形成了一个全新的函数,而这个函数就是一条从 g -> f 的流水线。同时我们可以很轻易的发现 compose 其实是满足结合律的
compose(f, compose(g, t)) = compose(compose(f, g), t) = f(g(t(x)))
只要其顺序一致,最后的结果是一致的,因此,我们可以写个更高级的 compose,支持多个函数组合:
compose(f, g, t) => x => f(g(t(x))
函数组合应用
考虑一个小功能:将数组最后一个元素大写,假设 log, head,reverse,toUpperCase 函数存在
const upperLastItem = compose(log, toUpperCase, head, reverse);
通过参数我们可以很清晰的看出发生了 uppderLastItem 做了什么,它完成了一套流水线,所有经过这条流水线的参数都会经历:reverse -> head -> toUpperCase -> log 这些函数的加工,最后生成结果。
面向对象和函数式编程冲突吗?
面向对象一直处于我能操作什么数据、这种数据我该怎么操作的范式中。而函数式编程一直沉浸于给我操作数据的方法中。面向对象最大优点是多态性和封装;函数式编程优势是抽象化和声明式命令风格,两者其实是正交,可互补的,可在同一程序中共存。争论是面向对象好还是面向函数好跟争论哪门语言好一样都是非常极端的。对于面向对象来讲:存在的并不一定都是对象,函数就是对象;对于函数式编程来说:存在的并不总是纯粹的,副作用总是真实存在的。总之,面向对象侧重于分解,函数编程侧重于组合。
总结
- 函数式编程是围绕高阶函数进行的,设计的核心在于高阶函数的设计。
- 函数式编程关心数据的映射,命令式编程关心解决问题的步骤
- 除了纯函数式编程语言外,还可以在非函数式编程语言中建立函数式编程方法。