关于函数式编程
函数式编程与面向对象编程和面向过程编程一样,属于一种编程范式。
关于后两者,这里贴一个知乎回答,大家自己体会:
面向对象是相对于面向过程的,比如你要充话费,你会想,可以下个支付宝,然后绑定银行卡,然后在淘宝上买卡,自己冲,这种种过程。但是对于你女朋友就不一样了,她是面向“对象”的,她会想,谁会充话费呢?当然是你了,她就给你电话,然后你把之前的做了一遍,然后她收到到帐的短信,说了句,亲爱的。这就是面向对象!女的思维大部分是面向“对象”的!她不关心处理的细节,只关心谁可以,和结果!
作者:知乎用户
链接:https://www.zhihu.com/question/31021366/answer/50581592
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
那么大名鼎鼎的函数式编程又是怎么回事呢?
定义不好下,我们可以通过几个特点来管中窥豹,略见一斑^_^
函数式编程的几个特点
1. 以函数作为主要载体。
函数地位最高,函数可以赋值给一个变量,函数可以作为参数,函数还可以作为返回值。
2. 只用表达式,不用语句。
"表达式"是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。
函数式编程的开发动机,一开始就是为了处理运算,不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以函数式编程排斥使用语句。
当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。
3. 没有副作用。
副作用,指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。
4. 引用透明。
引用透明,指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。
深入探究函数式编程
一、高阶函数
二、函数的合成与柯里化
函数式编程有两个最基本的运算:合成和柯里化。
1. 合成
合成像一系列管道那样把不同的函数联系在一起。例如:
// f 和 g 都是函数,x 是在它们之间通过“管道”传输的值。 const compose = function (f, g) { return function (x) { return f(g(x)) } } const toUpperCase = function (x) { return x.toUpperCase() } const exclaim = function (x) { return `${x}!` } const shout = compose(toUpperCase, exclaim) // 调用 compose 返回一个新函数 shout('functional programing is useful') // "FUNCTIONAL PROGRAMING IS USEFUL!"
2. 柯里化
所谓"柯里化",就是把一个多参数的函数,转化为单参数函数。
// 柯里化之前 function add(x, y) { return x + y; } add(1, 2) // 3 // 柯里化之后 function addX(y) { return function (x) { return x + y; }; } addX(2)(1) // 3
三、递归和尾递归
http://www.ruanyifeng.com/blog/2015/04/tail-call.html
1 函子
1.1 概念
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
它是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
上图中,左侧的圆圈就是一个函子,表示人名的范畴。外部传入函数f
,会转成右边表示早餐的范畴。
1.2 函子的代码实现
任何具有map
方法的数据结构,都可以当作函子的实现。
class Functor { constructor (val) { this.val = val } map (f) { return new Functor(f(this.val)) } } (new Functor(2)).map(x => x + 10) // Functor(12)
上面的例子说明,函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器----函子。函子本身具有对外接口(map
方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。
因此,学习函数式编程,实际上就是学习函子的各种运算。由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。
1.3 函子的 of 方法
上面生成新的函子的时候,用了new
命令。这实在太不像函数式编程了,因为new
命令是面向对象编程的标志。
函数式编程一般约定,函子有一个of
方法,用来生成新的容器。
使用 static 关键字给 Functor 添加一个静态方法 of ,使得 of 可以直接在 Functor
类 上调用,而不是在 Functor类 的实例上调用。
class Functor { constructor (val) { this.val = val } static of (val) { return new Functor(val) } map (f) { return new Functor(f(this.val)) } } Functor.of(2).map(x => x + 10) // Functor(12) Functor.of(2).map(x => x + 10).map(x => x + 100) // Functor(112)
2 Maybe 函子
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如null
),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Maybe 函子就是为了解决这一类问题而设计的。简单说,它的map
方法里面设置了空值检查。
// Maybe 函子 class Maybe { constructor (val) { this.val = val } static of (val) { return new Maybe(val) } map (f) { return this.val ? Maybe.of(f(this.val)) : Maybe.of(null) } } Maybe.of(null).map(s => s.toUpperCase()) // Maybe(null)
3 Either 函子
条件运算if...else
是最常见的运算之一,函数式编程里面,使用 Either 函子表达。
Either 函子内部有两个值:左值(Left
)和右值(Right
)。右值是正常情况下使用的值,右值不存在时使用左侧默认值。
// Either 函子 class Either { constructor (left, right) { this.left = left this.right = right } static of (left, right) { return new Either(left, right) } map (f) { return this.right ? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right) } } Either.of(5, 6).map(x => x + 1) // Either(5, 7) Either.of(1, null).map(x => x + 1) // Either(2, null)
4 ap 函子
一个函子的值是数值,另一个函子的值是函数。下面代码中,函子A
内部的值是2
,函子B
内部的值是函数addTwo
。
function addTwo(x) { return x + 2; } const A = Functor.of(2); const B = Functor.of(addTwo)
有时,我们想让函子B
内部的函数,可以使用函子A
内部的值进行运算。这时就需要用到 ap 函子。
ap 是 applicative(应用)的缩写。凡是部署了ap
方法的函子,就是 ap 函子。
class Functor { constructor (val) { this.val = val } static of (val) { return new Functor(val) } map (f) { return new Functor(f(this.val)) } } // ap函子 class Ap { constructor (val) { this.val = val } static of (val) { return new Ap(val) } ap (F) { return Ap.of(this.val(F.val)) } } function addTwo (x) { return x + 2 } Ap.of(addTwo).ap(Functor.of(9)) // Ap(11)
注意,ap
方法的参数不是函数,而是另一个函子。
ap 函子的意义在于,对于那些多参数的函数,就可以从多个容器之中取值,实现函子的链式操作。
// Maybe 函子 class Maybe { constructor (val) { this.val = val } static of (val) { return new Maybe(val) } map (f) { return this.val ? Maybe.of(f(this.val)) : Maybe.of(null) } } // ap函子 class Ap { constructor (val) { this.val = val } static of (val) { return new Ap(val) } ap (F) { return Ap.of(this.val(F.val)) } } function add (x) { return function (y) { return x + y } } Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3)) // Ap(5)
上面代码中,函数add
是柯里化以后的形式,一共需要两个参数。通过 ap 函子,我们就可以实现从两个容器之中取值。它还有另外一种调用写法。
Ap.of(add(2)).ap(Maybe.of(3)) // Ap(5)
5 Monad 函子
函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。但是,这样就会出现多层嵌套的函子。
Maybe.of( Maybe.of( Maybe.of({name: 'Mulburry', number: 8402}) ) )
上面这个函子,一共有三个Maybe
嵌套。如果要取出内部的值,就要连续取三次this.val
。这当然很不方便,因此就出现了 Monad 函子。
Monad 函子的作用是,总是返回一个单层的函子。它有一个flatMap
方法,与map
方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
// Monad函子 class Monad { constructor (val) { this.val = val } static of (val) { return new Monad(val) } join () { return this.val } flatMap (f) { return this.map(f).join() } map (f) { return new Monad(f(this.val)) } } function monadTest () { return Functor.of(3) } Monad.flatMap(monadTest) // Functor(3)
上面代码中,函数f
返回的是一个函子,那么this.map(f)
就会生成一个嵌套的函子。所以,join
方法保证了flatMap
方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平(flatten)。
6 IO 操作
Monad 函子的重要应用,就是实现 I/O (输入输出)操作。
I/O 是不纯的操作,普通的函数式编程没法做,这时就需要把 IO 操作写成Monad
函子,通过它来完成。
class IO { constructor(val) { this.val = val } static of(val) { return new IO(val) } join() { return this.val } flatMap(f) { return this.map(f).join() } map(f) { return new IO(f(this.val)) } run() { debugger if (typeof this.val === 'function') this.val() else console.log(this.val); } } const a = function (filename) { function a1() { setTimeout(() => { console.log(filename) }, 500); } return a1 } const b = function (x) { function b1() { if (typeof x === 'function') x() else console.log(x) return x; } return b1 } const c = function (x) { function c1() { if (typeof x === 'function') x() else console.log(x) return x; } return c1 } const readFile = function (filename) { return new IO(a(filename)); }; const print = function (x) { return new IO(b(x)); } const tail = function (x) { return new IO(c(x)); } readFile('a-file-name') .flatMap(print) .flatMap(tail) .run() // 最终会执行函数a1, 打印出'a-file-name'
上面的代码完成了不纯的操作,但是因为flatMap
返回的还是一个 IO 函子,所以这个表达式是纯的。我们通过一个纯的表达式,完成带有副作用的操作,这就是 Monad 的作用。
由于返回还是 IO 函子,所以可以实现链式操作。因此,在大多数库里面,flatMap
方法被改名成chain
。