编程范式 —— 函数式编程入门
source link: https://segmentfault.com/a/1190000018101201?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
该系列会有 3 篇文章,分别介绍什么是函数式编程、剖析函数式编程库、以及函数式编程在 React 中的应用,欢迎关注我的 blog
命令式编程和声明式编程
拿泡茶这个事例进行区分命令式编程和声明式编程
- 命令式编程
1.烧开水(为第一人称)
2.拿个茶杯
3.放茶叶
4.冲水
- 声明式编程
1.给我泡杯茶(为第二人称)
举个 demo
// 命令式编程 const convert = function(arr) { const result = [] for (let i = 0; i < arr.length; i++) { result[i] = arr[i].toLowerCase() } return result } // 声明式编程 const convert = function(arr) { return arr.map(r => r.toLowerCase()) }
什么是函数式编程
函数式编程是声明式编程的范式。在函数式编程中数据在由纯函数组成的管道中传递。
函数式编程可以用简单如 交换律、结合律、分配律
的数学之法来帮我们简化代码的实现。
它具有如下一些特性:
- 纯粹性: 纯函数不改变除当前作用域以外的值;
// 反面示例 let a = 0 const add = (b) => a = a + b // 两次 add(1) 结果不一致 // 正确示例 const add = (a, b) => a + b
- 数据不可变性: Immutable
// 反面示例 const arr = [1, 2] const arrAdd = (value) => { arr.push(value) return arr } arrAdd(3) // [1, 2, 3] arrAdd(3) // [1, 2, 3, 3] // 正面示例 const arr = [1, 2] const arrAdd = (value) => { return arr.concat(value) } arrAdd(3) // [1, 2, 3] arrAdd(3) // [1, 2, 3]
在后记 1 中对数组字符串方法是否对原值有影响作了整理
- 函数柯里化: 将多个入参的函数转化为一个入参的函数;
const add = a => b => c => a + b + c add(1)(2)(3)
- 偏函数: 将多个入参的函数转化成两部分;
const add = a => (b, c) => a + b + c add(1)(2, 3)
- 可组合: 函数之间能组合使用
const add = (x) => x + x const mult = (x) => x * x const addAndMult = (x) => add(mult(x))
柯里化(curry)
如下是一个加法函数:
var add = (a, b, c) => a + b + c add(1, 2, 3) // 6
假如有这样一个 curry
函数, 用其包装 add
函数后返回一个新的函数 curryAdd
, 我们可以将参数 a、b
进行分开传递进行调用。
var curryAdd = curry(add) // 以下输出结果都相同 curryAdd(1, 2, 3) // 6 curryAdd(1, 2)(3) // 6 curryAdd(1)(2)(3) // 6 curryAdd(1)(2, 3) // 6
动手实现一个 curry 函数
核心思路: 若传进去的参数个数未达到 curryAdd
的个数,则将参数缓存在闭包变量 lists 中:
function curry(fn, ...args) { const length = fn.length let lists = args || [] let listLen return function (..._args) { lists = [...lists, ..._args] listLen = lists.length if (listLen < length) { const that = lists lists = [] return curry(fn, ...that) } else if (listLen === length) { const that = lists lists = [] return fn.apply(this, that) } } }
代码组合(compose)
现在有 toUpperCase
、 reverse
、 head
三个函数, 分别如下:
var toUpperCase = (str) => str.toUpperCase() var reverse = (arr) => arr.reverse() var head = (arr) => arr[0]
接着使用它们实现将数组末位元素大写化输出, 可以这样做:
var reverseHeadUpperCase = (arr) => toUpperCase(head(reverse(arr))) reverseHeadUpperCase(['apple', 'banana', 'peach']) // "PEACH"
此时在构建 reverseHeadUpperCase
函数的时候, 必须手动声明传入参数 arr, 是否能提供一个 compose
函数让使用者更加友好的使用呢? 类似如下形式:
var reverseHeadUpperCase = compose(toUpperCase, head, reverse) reverseHeadUpperCase(['apple', 'banana', 'peach']) // "PEACH"
此外 compose
函数符合 结合律
, 我们可以这样子使用:
compose(compose(toUpperCase, head), reverse) compose(toUpperCase, compose(head, reverse))
以上两种写法与 compose(toUpperCase, head, reverse)
的效果完全相同, 都是依次从右到左执行传参中的函数。
此外 compose
和 map
一起使用时也有相关的结合律, 以下两种写法效果相等
compose(map(f), map(g)) map(compose(f, g))
动手实现一个 compose 函数
代码精华集中在一行之内, 其为众多开源库(比如 Redux) 所采用。
var compose = (...args) => (initValue) => args.reduceRight((a, c) => c(a), initValue)
范畴论
范畴论是数学中的一个分支。可以将范畴理解为一个容器, 把原来对值的操作,现转为对容器的操作。如下图:
学习函数式编程就是学习各种函子的过程。
函数式编程中, 函子(Functor)
是实现了 map
函数的容器, 下文中将函子视为范畴,模型可表示如下:
class Functor { constructor(value) { this.value = value } map(fn) { return new Functor(fn(this.value)) } }
但是在函数式编程中, 要避免使用 new
这种面向对象的编程方式, 取而代之对外暴露了一个 of
的接口, 也称为 pointed functor
。
Functor.of = value => new Functor(value)
Maybe 函子
Maybe 函子
是为了解决 this.value
为 null 的情形, 用法如下:
Maybe.of(null).map(r => r.toUpperCase()) // null Maybe.of('m').map(r => r.toUpperCase()) // Maybe {value: "M"}
实现代码如下:
class Maybe { constructor(value) { this.value = value } map(fn) { return this.value ? new Maybe(fn(this.value)) : null } } Maybe.of = value => new Maybe(value)
Either 函子
Either 函子
是为了对应 if...else...
的语法, 即 非左即右
。因此可以将之拆分为 Left
和 Right
两个函子, 它们的用法如下:
Left.of(1).map(r => r + 1) // Left {value: 1} Right.of(1).map(r => r + 1) // Right {value: 2}
Left 函子
实现代码如下:
class Left { constructor(value) { this.value = value } map(fn) { return this } } Left.of = value => new Left(value)
Right 函子
实现代码如下(其实就是上面的 Functor
):
class Right { constructor(value) { this.value = value } map(fn) { return new Right(fn(this.value)) } } Right.of = value => new Right(value)
具体 Either
函数只是对调用 Left 函子
或 Right 函子
作一层筛选, 其接收 f
、 g
两个函数以及一个函子( Left or Right
)
var Either = function(f, g, functor) { switch(functor.constructor) { case 'Left': return f(functor.value) case 'Right': return g(functor.value) default: return f(functor.value) } }
使用 demo:
Either((v) => console.log('left', v), (v) => console.log('def', v), left) // left 1 Either((v) => console.log('rigth', v), (v) => console.log('def', v), rigth) // rigth 2
Monad 函子
函子会发生嵌套, 比如下面这样:
Functor.of(Functor.of(1)) // Functor { value: Functor { value: 1 } }
Monad 函子
对外暴露了 join
和 flatmap
接口, 调用者从而可以扁平化嵌套的函子。
class Monad { constructor(value) { this.value = value } map(fn) { return new Monad(fn(this.value)) } join() { return this.value } flatmap(fn) { return this.map(fn).join() } } Monad.of = value => new Monad(value)
使用方法:
// join Monad.of(Monad.of(1).join()) // Monad { value: 1 } Monad.of(Monad.of(1)).join() // Monad { value: 1 } // flatmap Monad.of(1).flatmap(r => r + 1) // 2
Monad 函子可以运用在 I/O 这种不纯的操作上将之变为纯函数的操作,目前比较懵懂,日后补充。
后记 1: 数组字符串方法小结(是否对原值有影响)
不会对原数组有影响的方法
slice
var test = [1, 2, 3] var result = test.slice(0, 1) console.log(test) // [1, 2, 3] console.log(result) // [1]
concat
var test = [1, 2, 3] var result = test.concat(4) console.log(test) // [1, 2, 3] console.log(result) // [1, 2, 3, 4]
对原数组有影响的方法
splice(这个需要特别记一下)
var test = [1, 2, 3] var result = test.splice(0, 1) console.log(test) // [2, 3] console.log(result) // [1]
sort
var arr = [2, 1, 3, 4] arr.sort((r1, r2) => (r1 - r2)) console.log(arr) // [1, 2, 3, 4]
reverse
var test = [1, 2, 3] var result = test.reverse() console.log(test) // [3, 2, 1] console.log(result) // [3, 2, 1]
push/pop/unshift/shift
var test = [1, 2, 3] var result = test.push(4) console.log(test) // [1, 2, 3, 4] console.log(result) // 4
不会对原字符串造成影响的方法
substr/substring/slice
// substr var test = 'abc' var result = test.substr(0, 1) console.log(test) // 'abc' console.log(result) // a // substring var test = 'abc' var result = test.substring(0, 1) console.log(test) // 'abc' console.log(result) // a // slice var test = 'abc' var result = test.slice(0, 1) console.log(test) // 'abc' console.log(result) // a
参考
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK