说到函数式编程,想必各位或多或少都有所耳闻,然而对于函数式的内涵和本质可能又有些说不清楚。javascript
因此本文但愿针对工程师,从应用(而非学术)的角度将函数式编程相关思想和实践(以 JavaScript 为例)分享给你们。html
文章内容其实主要来自于在下阅读各种参考文献后的再整理,因此有什么错误也但愿你们帮忙斧正~java
slide 地址git
Functional programming is a programming paradigm程序员
1.treats computation as the evaluation of mathematical functionses6
2.avoids changing-state and mutable datagithub
by wikipedia数据库
从以上维基百科的定义来看有三个要点编程
Programming Paradigm
:编程范式Mathematical Functions
:数学函数Changing-state And Mutable Data
:改变状态和可变数据下面分别解析一下以上要点。数组
编程范式从概念上来说指的是编程的基本风格和典范模式。
换句话说其实就是程序员对于如何使用编程来解决问题的世界观和方法论。
若是把一门编程语言比做兵器,它的语法、工具和技巧等是招法,那么它采用的编程范式也就是是内功心法。
一种范式能够在不一样的语言中实现,一种语言也能够同时支持多种范式。例如 JavaScript 就是一种多范式的语言。
通常的,在一个变化过程当中,假设有两个变量 x、y,若是对于任意一个 x 都有惟一肯定的一个y和它对应,那么就称 x 是自变量,y 是 x 的函数。x 的取值范围叫作这个函数的定义域,相应 y 的取值范围叫作函数的值域。
以上定义,在初中数学我们都应该学过...
换句话说,函数只是两种数值之间的关系:输入和输出。
尽管每一个输入都只会有一个输出,但不一样的输入却能够有相同的输出。下图展现了一个合法的从 x 到 y 的函数关系;
与之相反,下面这张图表展现的就不是一种函数关系,由于输入值 5 指向了多个输出:
纯函数是这样一种函数,对于相同的输入,永远会获得相同的输出,并且没有任何可观察的反作用。
根据定义能够看出纯函数其实就是数学函数,即表示从输入的参数到输出结果的映射。
而没有反作用的纯函数显然都是引用透明的。
引用透明性(Referential Transparency)指的是,若是一段代码在不改变整个程序行为的前提下,能够替换成它的执行结果。
const double = x => x * 2
const addFive = x => x + 5
const num = double(addFive(10))
num === double(10 + 5)
=== double(15)
=== 15 * 2
=== 30
复制代码
不过说了半天,反作用又是啥...?
反作用是在计算的过程当中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
反作用可能包含,但不限于如下行为:
只要是跟函数外部环境发生的交互就都是反作用——这一点可能会让你怀疑无反作用编程的可行性。
函数式编程的哲学就是假定反作用是形成不正当行为的主要缘由。
固然这并非说,要禁止使用一切反作用,而是说,要让它们在可控的范围内发生。
在后面讲到函子(functor)和单子(monad)的时候咱们会学习如何控制它们。
面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只须要一个香蕉,但却获得一个拿着香蕉的大猩猩...以及整个丛林
by Erlang 做者:Joe Armstrong
因此使用纯函数将会有如下好处:
Shared mutable state is the root of all evil
共享可变状态是万恶之源
by Pete Hunt
const obj = { val: 1 }
someFn(obj)
console.log(obj) // ???
复制代码
from Building Scalable, Highly Concurrent & Fault Tolerant Systems - Lessons Learned
说到函数式编程语言,你们的第一反应多是 Haskell、OCaml、Lisp、Erlang、Scala、F#...
由于它们可能有如下特性:
而说到 JavaScript,不少人可能第一反应认为这是一门面向对象的语言。
可是想一想前面说的:函数式编程只是一种编程范式,而编程范式就像“内功心法”,因此与以上这些语言特性不彻底相关,反而与你本身的编程思惟(即世界观和方法论)更加相关。
在函数式方面,因为 JavaScript 支持高阶函数、匿名函数、函数是一等公民、闭包、解构(模式匹配)等特性,因此它也能支持函数式编程范式。(虽然不是那么的原教旨函数式,但还基本够用~尤为是 ES6 新增的箭头函数等特性~还有各类类库 )
事实上 JavaScript 是一门基于原型(prototype-based)的多范式语言。
JavaScript 一共有 6 种原始类型(包括 ES6 新添加的 Symbol 类型),它们分别是 Boolean,Null,Undefined,Number,String 和 Symbol。 除了这些原始类型,其余的类型都是 Object,而 Object 都是可变的。
惰性(lazy)指求值的过程并不会马上发生。
好比一些数学题,咱们可能一开始并不须要把全部表达式都求值,这样能够在计算的过程当中将一些表达式消掉。
惰性求值是相对于**及早求值(eager evaluation)**的。
好比大部分语言中,参数中的表达式都会被先求值,这也称为应用序语言。
好比看下面这样一个 JavaScript
的函数:
wholeNameOf(getFirstName(), getLastName())
复制代码
getFirstName
与 getLastName
会依次执行,返回值做为 wholeNameOf
函数的参数, wholeNameOf
函数最后才被调用。
另外,对于数组操做时,大部分语言也一样采用的是应用序。
[1, 2, 3, 4].map(x => x + 1)
复制代码
因此,这个表达式马上会返回结果 [2, 3, 4, 5] 。
固然这并非说 JavaScript
语言使用应用序有问题,可是没有提供惰性序列的支持就是 JavaScript
的不对了。若是 map
一个大数组后咱们发现其实只须要前 10 个元素时,去计算全部元素就显得多余了。
面向对象一般被比喻为名词,而函数式编程是动词。面向对象抽象的是对象,对于对象的的描述天然是名词。
面向对象把全部操做和数据都封装在对象内,经过接受消息作相应的操做。好比,对象 Kitty,它们能够接受“打招呼”的消息,而后作相应的动做。
而函数式的抽象方式恰好相反,是把动做抽象出来,好比“打招呼”就是一个函数,而函数参数就是做为数据传入的 Kitty(即 Kitty 进入函数“打招呼”,出来的应该是 Hello Kitty)。
面向对象能够经过继承和组合在对象之间分享一些行为或者说属性,函数式的思路就是经过组合已有的函数造成一个新的函数。
然而 JavaScript 语言虽然支持高阶函数,可是并无一个原生的利于组合函数产生新函数的方式。而这些强大的函数组合方式却每每被相似 Underscore,Lodash 等工具库的光芒掩盖掉(后面会说到这些库的问题)。
函数式编程语言中由于不可变数据结构的缘由,没办法实现循环。因此都是经过递归来实现循环。
然而递归使用不当很容易栈溢出(Stack Overflow),因此通常采用尾递归的方式来优化。
虽然 ES6 规范中规定了尾递归优化规范,然而提供实现的解释器还很是的少,详情能够查阅这个连接
JavaScript 做为一种弱类型的语言,没有静态类型系统。不过使用一些 TypeScript 等预编译的语言能够做为补充~
Declarative VS Imperative,这二者的区别简单来讲其实就是 What VS How。
声明式:
表达式是指一小段代码,它用来计算某个值。表达式一般是某些函数调用的复合、一些值和操做符,用来计算出结果值。
命令式:
语句是指一小段代码,它用来完成某个行为。通用的语句例子包括 for、if、switch、throw,等等……
// 命令式
function mysteryFn (nums) {
let squares = []
let sum = 0 // 1. 建立中间变量
for (let i = 0; i < nums.length; i++) {
squares.push(nums[i] * nums[i]) // 2. 循环计算平方
}
for (let i = 0; i < squares.length; i++) {
sum += squares[i] // 3. 循环累加
}
return sum
}
// 以上代码都是 how 而不是 what...
// 函数式
const mysteryFn = (nums) => nums
.map(x => x * x) // a. 平方
.reduce((acc, cur) => acc + cur, 0) // b. 累加
复制代码
// 命令式
function mysteryFn(nums) {
let sum = 0
let tally = 0 // 1. 建立中间变量
for (let i = 0; i < nums.length; i++) {
if (nums[i] % 2 === 0) { // 2. 循环,值为偶数时累加该值的一半并记录数量
sum += nums[i] / 2
tally++
}
}
return tally === 0 ? 0 : sum / tally // 3. 返回平均值
}
// 函数式
const mysteryFn = (nums) => nums
.filter(x => x % 2 === 0) // a. 过滤非偶数
.map(x => x / 2) // b. 折半
.reduce((acc, cur, idx, { length }) => (
idx < length - 1
? acc + cur // c. 累加
: (acc + cur) / length // d. 计算平均值
), 0)
复制代码
以上 to be continued...