关于函数式编程:用JavaScript入门函数式编程刚入门趁热分享

40次阅读

共计 5873 个字符,预计需要花费 15 分钟才能阅读完成。

函数式编程

命令式和申明式

咱们入门编程的时候,通常都是从命令式编程开始,即最简略的过程式代码。起初为了解决大型项目又接触到面向对象,也属于命令式。而函数式编程属于申明式,其实它的呈现早于面向对象。

MySQL 就是一个很好的申明式语言的例子,它仅仅是申明了流程,却没有将过程的细节裸露进去。

SELECT * from data_base WHERE author='jack';

再举一个例子来比拟命令式和申明式的代码:

const arr = [1, 2, 3, 4, 5]

// 要求将下面的数组中小于 3 的去掉,并且将剩下的数乘上 2

// 命令式
const result = []
for (let i=0; i<arr.length; i++) {if (arr[i] >= 3){result.push(arr[i] * 2)    
    }
}

// 申明式
const result = arr.filter(n >= 3).map(n => n*2)

看了下面的例子你可能会想,申明式的代码就这?再看看下面的 MySQL 的例子,申明式确是如此,通过 filter、map 等办法,封装了细节,而后将逻辑申明进去,并不需要一步一个命令地阐明要怎么做,所以更简洁,而这就是最根本的申明式代码。

当咱们学面向对象的时候,都会先晓得它的三个个性:封装、继承、多态。基于这三个个性,人们在编写代码会延申出很多最佳实际,即设计模式,如工厂模式、依赖注入等。而函数式编程也是如此,有对应的个性和最佳实际。

函数式

函数式编程是以函数为主的编程格调,其实程序就是由一段段逻辑组成,而将这些逻辑宰割成一个个函数,再将其组合施展到极致。

函数式解决了一个问题,当命令式的格调写代码时,一开始你能够很间接的实现工作代码,但当你开始思考边界解决和代码复用,慢慢的,你的代码会逐步背负它本不该有的复杂度,而函数式编程能解决这个问题。

程序的实质,除了是数据结构和算法,还能够是计算和副作用。

// 先看个例子,从 localStorage 中取出所有用户数据,并找出年龄最大的,显示在 DOM 上
const users = localStorage.getItem('users')
const sortedUsers = JSON.parse(users).sort((a, b) => b.age - a.age)
const oldestUser = sortedUsers[0]
document.querySelector('#user').innerText = oldestUser.name

下面的代码很平时,然而了解起来却须要肯定工夫,先做个函数式的优化,将下面所有步骤封装一下。

const getLocalStorage = (key) => localStorage.getItem(key);
const getSortedUser = users => users.sort((a, b) => b.age - a.age)
const first = arr => arr[0]
const writeDom = selector = text => document.querySelector(selector).innerText = text;
const prop = key = obj => obj[key]

writeDom('#user')(prop('name')(first(getSortedUser(JSON.parse(getLocalStorage('user'))))))

我晓得下面的代码看起来比拟奇怪,一是最终调用逻辑须要从右向左看,二是 writeDom、prop 这样分屡次将参数传入,即柯里化。只管它看起来奇怪,逻辑上的确比之前更清晰易读,并且所有函数都灵便、易扩大。

这就是函数式代码,强调申明式的代码以及函数的灵便组合。函数式编程的指标是应用函数来形象作用在数据之上的控制流与操作,从而在零碎中打消副作用并缩小对状态的扭转。前面会更加具体地论述这点。

上面对代码进一步优化。

柯里化

柯里化即把一次传入多参数的函数,转换成能分次传入参数的函数。然而当初咱们须要探讨一个参数先后顺序的问题,比方对 Array.map 进行柯里化封装,不同的参数程序会导致不同的成果:

const listMap = fn => list => list.map(fn)
const allAddOne = listMap((o) => o + 1)
allAddOne([1, 2]) // [2, 3]

// 如果反过来会是上面这样

const listMap2 = list => fn => list.map(fn)
const mapList = listMap2([1, 2])
mapList(o => o + 2) // [2, 3]

前者先传 fn 再传 list 更合乎函数式的规定,因为 函数式是针对逻辑的组合,而不是针对数据的组合。这也解答了为什么后面的例子须要把局部函数柯里化,这样更不便组合。

优化代码——函子(functor)

其实我以前就写过如下的申明式代码:

// [1, 2] => [1, 2, 3] => [2, 3] => [6, 9]
[1, 2]
    .concat([3])
    .filter(x => x > 1)
    .map(x => x * 3)

我以前写的时候就感觉这种链式调用的写法十分的清晰,要是所有逻辑都能够这么写就好了,而函数式编程就是这么个思路。如果咱们要解决非数组时,其实咱们能够用一个数组将其包裹,如

// ['123'] => ['123'] => [123] = [124] ⇒ 124
['123']
    .map(o => o.trim())
    .map(o => Number(o))
    .map(o => o + 1)
    .pop() // 124

然而下面这么做并不优雅,每次都要包裹在数组里。所以咱们能够创立一个蕴含 map 办法的对象,从而实现链式调用:

const Box = (v) => {
    return {map(fn) { // 将数据放进盒子
            return Box(fn(v)) 
        }, 
        getValue() { // 从盒子中取出数据
            return v 
        }
    }
}

// 重写下面的例子
Box('123')
    .map(o => o.trim())
    .map(o => Number(o))
    .map(o => o + 1)
    .getValue() // 124

// 重写后面的例子
Box(getLocalStorage('user'))
    .map(JOSN.parse)
    .map((o) => o.sort((a, b) => b.age - a.age))
    .map(first)
    .map(prop('name'))
    .map(writeDom('#user'))

// 比起之前的一长串,这样看起来清新多了

而这个 带 map 办法的 Box 在函数式编程中称为函子(functor),即一个包裹着数据的容器,它提供链接数据操作的能力。

进一步实际——Maybe

但下面的例子还有个问题,就是 getLocalStorage 不肯定会返回无效数据,也可能是 undefined,这样会导致后续报错。所以,最好就是在遇到指标不存在的状况时,能跳过接下来的所有操作。

函数式的思路是,咱们写两个 Box,一个叫 Just,他会失常解决所有流程,另一个叫 Nothing,它会主动跳过所有的流程,只须要在一开始判断用哪一个 Box 即可。

// 有值
const Just = (val) => ({map: (f) => Just(f(val)),
    getValue: () => val,
    isJust: () => true}) 
// 无值
const Nothing = () => ({map: (f) => Nothing(), // Nothing 不会执行后续的所有操作
    getValue: (defaultVal) => defaultVal,
    isJust: () => false}) 
// 再用一个 Maybe 来判断应用两个非凡的 Box 中的哪一个
const Maybe = (val) => val === null || val === undefined ? Nothing() : Just(val)

// 用 Maybe 重写后面的例子
Maybe(getLocalStorage('user'))
    .map(JOSN.parse)
    .map((o) => o.sort((a, b) => b.age - a.age))
    .map(first)
    .map(prop('name'))
    .map(writeDom('#user'))

下面的 Maybe 也叫做 Monad,是基于函子(Box)所实现的,能够了解为 Monad 是专门解决某些场景的函子,相似的 Monad 还有很多,用于处理函数式下遇到的各种场景。前面还会介绍另一个常见的 Monad。

纯函数和副作用

其实 React 中每一个渲染函数都是一个纯函数,相当于 UI = Funtion(state)。所以每次批改 state 后,React 的会从新跑一边渲染函数,只有传入一样参数,无论调用多少次,渲染进去的 UI 都是统一的,这就是纯函数

纯函数有什么长处呢?1、无副作用,可任意搁置,不影响上下文,易于组合;2、易于保护和重构,只有输入输出统一,轻易改都不会影响内部;3、输入稳固,易于单元测试;4、输出和输入齐全对应,便于缓存,只需判断输出是否更改就行。

然而纯函数的另一面就是副作用,因为当初的程序在运行中必然须要做 IO 操作(申请、操作 DOM 等),这些就是所谓的副作用,如果一个函数蕴含了副作用,那么就无奈做到屡次调用后果统一,指不定哪一次外部的变量就被副作用批改了。

而 React 解决副作用的形式是应用 useEfffect,来将副作用收集起来,尽管无奈将副作用齐全去除,但能够收集起来对立管控。这也是为什么 useEffect 内的函数并非在执行渲染函数过程中就执行,而是保护成队列,在渲染完后再执行,就是为了对立解决副作用,放弃渲染函数的污浊。

那么在个别的函数式编程中,如何解决副作用呢?

副作用——IO Monad

跟 React 的思路一样,集中处理。将副作用包裹在函子中,并增加一个 runIO 办法,这样有个益处,就是在最终 runIO 办法调用时才执行,这样就能很平安地拿捏副作用了。

const IO = (fn) => {
    return {map(fn) {return IO(() => fn(sideEffectFn())) // 让所有副作用提早执行
        },
        runIO() { // 显示调用 IO,使得咱们能够将 IO 操作更显著地放在一起,方便管理
            fn()},
    }
}

// 例子:const getNumDom = () => document.querySelector('#num')
const writeNum = (text) => {document.querySelector('#num').innerText = tex
}
const ioEffect = Maybe(getLocalStorage('user'))
                .map(JOSN.parse)
                .map((o) => o.sort((a, b) => b.age - a.age))
                .map(first)
                .map(prop('name'))
                .map(o => IO(() => writeDom('#user')(o)))
 
ioEffect.getValue().runIO() // 一次运行副作用

这样除了能够集中把控副作用外,还能够将读数据、解决数据、写数据很清晰的拆散,并且更利于浏览和保护

但下面返回的是 Maybe(IO(o)),Monad 呈现了嵌套,导致最终调用冗余:ioEffect.getValue().runIO()。这时候咱们只须要给 Maybe 加一个 foldMap 办法即可,这个办法目标是在传入下一个 Monad 时接触嵌套。

...
foldMap(monad) {return monad(val)
}
...

// 重写下面嵌套的例子

const ioEffect = Maybe(getLocalStorage('user'))
                .map(JOSN.parse)
                .map((o) => o.sort((a, b) => b.age - a.age))
                .map(first)
                .map(prop('name'))
                .foldMap(o => IO(() => writeDom('#user')(o)))

ioEffect.runIO()

通过 foldMap 能够解决 Monad 嵌套的问题,所以 foldMap 就是 Monad 必须的一个办法

组合函数,让数据的流动更简洁

Box.map(fn).map(fn) 尽管看起来很清晰,但其实还有一种写法能组合函数,就是写一个办法将所有函数组合起来,省去 map,看起来更简洁。尝试写一下这个办法,就叫它 pipe,像水管一样组合函数:

const pipe = (...fns) => (arg) => fns.reduce((lastVal, fn) => fn(lastVal), arg)

为了能将 Monad 也放进 pipe 中,咱们须要再封装一下 map 和 foldMap 这两个必要的办法:

const map = fn => monad => monad.map(fn)
const foldMap = fn => monad => monad.foldMap(fn) 

// 重写后面的例子
const ioEffect = pipe(
  getLocalStorage,
    Maybe,
    map((o) => o.sort((a, b) => b.age - a.age))
  map(first)
  map(prop('name'))
  foldMap(o => IO(() => writeDom('#user')(o)))
)('user')

ioEffect.runIO()

利用成熟库

当初市场上两个比拟成熟的 JavaScript 工具库—— lodash/fp 和 Ramda,它们所蕴含的工具函数都实现了柯里化,并且是默认先传函数再传被解决的数据。柯里化再配合下面的 pipe 办法,则能够像上面这样写:

const arr = [{n: '5'}, {n: '12'}]

// 日常写法
arr
    .map(o => o.n)
    .map(o => Number(o))
    .filter(o => o < 10) // => [5]

// 配合 lodash/fp
import {map, filter} from 'lodash/fp'
pipe(map('n'), 
    Number, 
    filter(o => o < 10)
)(arr)

这种写法也叫做 point free,即只思考函数组合,并不需要思考参数什么时候传入,因为最终它们会造成一个管道,一头入参,另一头天然就呈现后果,而两头的过程是能够任意替换的。

参考:
https://llh911001.gitbooks.io…
https://book.douban.com/subje…
https://egghead.io/lessons/ja…
https://www.ruanyifeng.com/bl…

正文完
 0