函数式编程(FP)

更新日期: 2022-02-16阅读: 1.1k标签: 编程

写在前面

可能大家都听过武侠小说中的 内功 和 招式 ,商业大佬讲的 道 与 术 ,一些唱歌选秀评委口中的 感情 和 技巧 。

程序员的江湖里是不是也存在没有感情的 api 调用工程师 。随着前端生态的迅速发展,目前框架语法、提案都更新换代的很快。各种各样的招式层出不穷,让人应接不暇,身心俱乏,对于内力的领悟和沉淀已经迫在眉睫,因为不管框架 API 怎么变一些编程的内在思想是不会变的。

js 为了实现面向对象的思想,做了很多事情,导致大家在学习 js 的时候,会遇到复杂的原型、原型链、继承,还有对人不友好的 this ;而当我们用这些东西组合起来模拟面向对象的特性的时候,就更加痛苦了。但我们可以使用一种更友好的方式,函数式编程。

什么是函数式编程

函数式编程(functional programing)是编程范式之一。我们常见的范式还有面向过程、面向行为、面向对象等。

范式:我们可以认为它是一种思维模式加上它的实现方法,简单说就是编程的方法论。

面向过程编程:简单解释就是按照步骤来实现。

面向行为编程:它是函数式编程的衍生范型,将电脑运算平展为一系列的变化,并且避免使用程序指令以及堆叠的对象。

面向对象编程:它的思维方式是把现实世界中的事物抽象成程序世界中的类和对象,然后通过封装,继承和多态来演示事物之间的联系。

面向函数式编程:它的思维方式是把现实世界中的事物和事物之间的联系,抽象到程序世界中。

函数式编程特点:

程序的本质:就是利用计算机的计算能力将 输入 转化成对应的 输出 。

函数式编程中的 函数 指的不是编程语言里的函数,而是数学意义上的映射关系。比如 y=sin(x) 中 x 和 y 值的映射关系。

纯函数:相同的输入获得相同的输出(无副作用)。

函数式编程就是对 数据(函数) 映射关系的抽象。 举个例子:

比如我们已知 a,b 两个直角边,求斜边长度。

y = \sqrt{a^{2}+b^{2}}

//非函数式 y = (a^2 + b^2)^0.5
const a = 3;
const b = 4;
const y = Math.sqrt(a*a + b*b)

//函数式 y = f(a, b)
const f = (a, b) => {
    return Math.sqrt(a * a + b * b)
}
const y = f(3, 4)

通过代码实现,我们可以看出函数式就是对过程变形关系的抽象。抽象的是处理过程,然后我们只需关注输入和输出。接下来我们看一下几种函数式编程应用。

高阶函数 (high-order-function)

一个以函数作为参数或返回的函数。 高阶函数,它虽然听起来很复杂,但其实并不难。并且非常的实用。要完全理解这个概念,首先必须了解 头等函数 ( First-Class Functions )的概念。

头等函数简单的讲就是 函数也是一个对象,它能赋值给变量,能作为参数返回 。

而高阶函数就是以函数为参数或返回的函数。

// 一个批量处理数组元素的例子
const use = ( arr, fn ) => {
  return arr.map( i => fn(i))
}

闭包 (closure)

函数和其周围词法环境的引用捆绑在一起形成闭包。 闭包的概念并不复杂,只是定义比较绕。举一段代码的:chestnut:

function once(fn){
  let done = false;
  return function (){
    if(!done){
      done = true;
      fn.apply(this, arguments)
    }
  }
}

const logOnce = once(console.log)

//此时只会执行一次
logOnce(1)
logOnce(1)

闭包的本质是函数在执行时,会被放到执行栈上去执行,执行结束后被移除, 但是堆上作用域成员由于外部的引用而不能被释放 。因此内部函数依然可以访问外部函数的成员。

可能有的同学会问,为什么有引用不会被释放?这是因为 js 的 垃圾回收 机制中最常用的是标记清除和引用计数。这里我们就不展开,有兴趣的同学可以自行了解一下。

纯函数 (pure function)

相同的输入会得到相同的输出,而且没有任何可观测的副作用。 举一个数组中纯函数和不纯函数的 :chestnut:

let numberArr = [1,2,3,4,5]
//纯函数
numberArr.slice(0,2) //[1,2]
numberArr.slice(0,2) //[1,2]
//不纯函数
numberArr.splice(0,2) //[1,2]
numberArr.splice(0,2) //[3,4]

函数式编程不会保留计算中间的结果,所以变量是不可变、无状态的。我们可以把一个函数的执行结果交给另一个函数去处理。有的时候我们会拆分很多细粒度的函数库,这里可以了解一下 lodash 功能库,它提供了丰富的对数组、数字、对象、字符串、函数等操作的方法。

纯函数的好处:

对于耗时的操作,可对执行结果 缓存 ,提高代码性能。

方便测试,降低排查问题的难度。

在多线程环境下(web worker),可对共享内存数据任意执行。

柯里化 (currying)

假设一个场景,我们需要写一个函数来判断一个人的年龄是否大于 18 岁。你可能会直接写:

const lucy = {age: 17}
const bob = {age: 100}

function checkAge(age){
  return age > 18
}

checkAge(lucy.age)
checkAge(bob.age)

这样没什么问题,但是我们如果要更改基准值的时候判断是否大于 20 ,那可能有需要重新定义一个 checkAge20 的新函数了。如果这个基准一直在变... 有的同学立马就想到了,那我传入基准值就好啦。

const lucy = {age: 17}
const bob = {age: 100}

function checkAge(min, age){
  return age > min
}

checkAge(18, lucy.age)
checkAge(18, bob.age)

这样 checkAge 没什么问题,但是发现我们每次都需要输入重复的基准值。有没有什么办法可以避免重复呢?让我们试试使用闭包和高阶函数:

function checkAge(min){
  return function(age){ // 函数作为返回
    return age > min; // 闭包,引用外部参数
  }
}

// 若果用es6的语法会更简洁
const checkAge = min => age => age > min

const checkAge18 = checkAge(18)

checkAge18(lucy.age)

其实我们改造道这里就是函数的柯里化。那什么是柯里化呢?

当函数有多个参数的时候,我们可以对函数进行改造,只接收部分参数,然后返回一个函数继续等待接收剩余参数,并且返回相应的结果。

lodash 中的 FP


在lodash的官网上,我们很容易找到一个 function program guide 。在 lodash / fp 模块中提供了实用的对函数式编程友好的方法。里面的方式有以下的特性:

不可变

已柯里化(auto-curried)

迭代前置(iteratee-first)

数据后置(>例如:(CAN YOU FEEL MY WORLD --> can-you-feel-my-world)

import _ from 'lodash'

const str = "CAN YOU FEEL MY WORLD"
const split = _.curry((sep, str)=>_.split(str, sep))
const join = _.curry((sep, arr)=>_.join(arr, sep))
const map = _.curry((fn, arr)=>_.map(arr, fn))

const f = _.flow(split(' '),map(_.toLower), join('-'))

f(str) //'can-you-feel-my-world'

我们在使用 lodash 时,做能很多额外的转化动作,那我们试试 fp 模块吧。

import fp from 'lodash/fp'const str = "CAN YOU FEEL MY WORLD"const f = fp.flow(fp.split(' '),fp.map(fp.toLower), fp.join('-'))f(str) //'can-you-feel-my-world'

这种编程方式我们称之为 PointFree,它有 3 个特点:

不需要指明处理的数据

只需要合成运算过程

需要定义一些辅助的基本运算函数 当然使用的时候还是需要注意一下参数的描述。官网上有一个 :chestnut: 是这样的:

// The `lodash/map` iteratee receives three arguments:// (value, index|key, collection)_.map(['6', '8', '10'], parseInt);// ➜ [6, NaN, 2]// The `lodash/fp/map` iteratee is capped at one argument:// (value)fp.map(parseInt)(['6', '8', '10']);// ➜ [6, 8, 10]

FP 中的 map 方法和 lodash 中的 map 方法参数的个数是不同的。

什么是函数组合

弄明白了柯里化,我们开始函数组合了。

开发过程中,有的同学使用 高阶函数 和 高阶组件 的时候很容易写出洋葱代码。


withRouter(Form.create()(connect(({ model }) => ({ status: model.status}))(Index)))

这段代码通常我们会使用 装饰器 (decorator)的方案优化掉。

withRouter@Form.create()@connect(({ model }) => ({  status: model.status,}))Index

但是 装饰器 只适用于组件 Component,对于拥抱 hooks 的函数组件并不适用。

在 redux 和 lodash 都有函数组合的方法提供,分别是 compose 和 flow, fn = compose(f1,f2,f3) ,他可以帮助我们将上面的洋葱代码改造成管道的形式。我们需要注意管道的执行顺序,默认都是从右到左执行。compose 的实现也是特别的简单的。

function compose(...args){  return function(value){    return args.reverse().reduce(function(acc,fn){      return fn(acc)    }, value)  }}// es6const compose = (...args) => value => args.reverse().reduce((acc,fn) => fn(acc), value)

对于函数组合,我们也可以随时插入一些用来调试的函数。

const log = curry((label, x) => { console.log(label, x); return x; });const y = compose(c, log('after a'),b, log('after a'), a);

函子(Functor)

到目前来说,我们已经了解了一定的函数式编程的基础,但是我们还没有演示在函数式编程中如何把副作用控制在可控范围内、异常处理、异步操作等。在处理副作用之前,先聊下函子。

什么是函子?

容器:包容值和值的变形关系(这个变形关系就是函数)。

函子:一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法, map 方法可以运行一个函数对值进行处理(变形关系)。

下面我们通过一段代码来看一下:

class Container{  constructor(value){    //私有的值 不对外公布    this._value = value  }  //接收一个处理值的函数  map(fn){// map 是一个契约名称 fn 需要是一个纯函数    //返回一个新的函子    return new Container(fn(this._value))  }}new Container(1)  .map(x => x + 1)  .map(x => x * x)

这样我们可以通过创建时给定初始值,map 方法来修改这个值。但是一直使用 new 关键字,让代码看起来很面向对象,让我们来改造一下。

class Container{  //of 的作用就是给我们返回一个函子对象,我们把 new 关键字封装在里面  static of(value){    return new Container(value)  }    constructor(value){    this._value = value  }    map(fn){    return new Container(fn(this._value))  }}new Container.of(1)  .map(x => x + 1)  .map(x => x * x)

但是这样的一个基础的函子还是存在许多的问题,比如初始化的值与操作的方法不匹配、异常处理、可控副作用、异步执行等。因此衍生出一系列的函子来解决这些问题,这里罗列一下对应的函子和它们解决的问题:

maybe 函子 : 空值问题

Either 函子 :异常处理

IO 函子 :副作用处理

Task 函子 :异步执行

Monad 函子 :IO 函子多层嵌套

主流框架、库中的应用

在 Redux 中,要写一个中间件代码大致是这样的:

const middleware = store => next => action => {  // 具体实现};

其实对于最后的实现主体来说无非都是拿到 storenextaction 三个参数而已。完全可以用下面的方式定义:

(store, next, action) => {...}

但是作者 Dan Abramov 还是采用了更具有函数式特性的方式去定义。

另外,react 16.8 版本开始正式的支持了 hooks。hooks 对比类组件的写法有几处优势这也刚好是符合函数式编程的特性的。

通过自定义 hooks 来共享一些组件的逻辑,如果用类组件实现,只能通过高阶组件模拟,这样会不断嵌套,无用的“龟壳”。

每个方法都是 独立 的, 不需要像类组件那样在一个 mount 生命周期里做一堆不相关的操作,更新时又做一堆不相关的操作。不相关的逻辑整合在一个生命周期内,本来就是不易读、不易维护的。

class Example extends Component {  componentDidMount() {    //注册事件    //请求Api    //设置状态 等等  }  componentWillUnmount(){    //取消一些监听事件  }}

而 Hooks(主要是 useEffect)取代了生命周期的概念,让代码的依赖逻辑更接近本质。 函数式编程为组件的编写提供了更高的灵活度与可读性 。

总结

函数式编程是一种范式、一种思想、一种约定。他有着一定的优势,更高的可组合性,灵活性以及容错性。但是在实际应用中是很难用函数式去表达的,我们应该将其当做我们现有储备的一种补充,而并非最优解去看待。以往的开发过程,我们可能习惯了用变量存储和追踪程序的状态,不停的在一些节点打印语句来观察程序的过程,现代的 JavaScript 库已经开始尝试拥抱函数式编程的概念以获取这些优势来降低系统复杂度。统一存储管理数据,将程序的运行状态置于可预见状态里。 React、Rxjs、Redux 等 js 库都是这一理念的最佳实践者。

本文首发于政采云前端团队博客:函数式编程(FP)
https://www.zoo.team/article/function-production

链接: https://fly63.com/article/detial/11175

程序员的笔记,编程写软件学到的 7 件事

如果你真的做出了一些东西,在面对那些令人眼花缭乱的理论知识,或是和你相似甚至比你做的更糟糕的人时大可不必谦虚。在一天结束之时,正是那些在战壕中的开发者——构建、测试和开发了代码的人,真正做了事情。

自学编程的六个技巧总结

这些事情可以帮助新手在他们漫长的旅程中学习编程。我知道我还有更多东西需要学习,并将继续学习如何永远地学习。最重要的事情说三遍,请继续,不要放弃,不要放弃,不要放弃。

谈谈Javascript异步代码优化

Javascript代码异步执行的场景,比如ajax的调用、定时器的使用等,在这样的场景下也经常会出现这样那样匪夷所思的bug或者糟糕的代码片段,那么处理好你的Javascript异步代码成为了异步编程至关重要的前提

编程到底难在哪里?

以买苹果为例说明程序员如何解决问题。程序员需要对问题进行透彻的分析,理清其涉及的所有细节,预测可能发生的所有意外与非意外的情况,列出解决方案的所有步骤,以及对解决方案进行尽量全面的测试。而这些正是我认为编程难的地方。

Blockly - 来自Google的可视化编程工具

Google Blockly 是一款基于Web的、开源的、可视化程序编辑器。你可以通过拖拽块的形式快速构建程序,而这些所拖拽的每个块就是组成程序的基本单元。可视化编程完成

我真是受够编程了

成为伟大的程序员,需要付出许多编程之外的努力。我们的大脑是有限的,每天要应付的问题复杂到足以让人精神崩溃。当工作不顺利时,多少都会有些冒名顶替症候群的感觉。

前端的编程软件哪些比较好用?

推荐8款最好用的前端开发工具供美工或者前端开发人员使用,当然若你是NB的全栈工程师也可以下载使用。Web前端开发最常见的编程软件有以下几种: 在前端开发中,有一个非常好用的工具,Visual Studio Code,简称VS code

如何保持学习编程的动力

学编程现在看起来挺简单,因为网上有丰富的各种资源。然而当你实际去学的时候就发现,还是很难!对我来说也一样。但从某天起,我决定认认真真学编程一年。后来又过了一年,又过了一年又一年……我好像有点感悟。

编程小技巧

命名最好遵循驼峰法和下划线法,并且要清楚的表达变量的意思。相对于驼峰法而言,我更喜欢下划线法。下划线法可以更清楚的看出这个变量表示的意思。比如aBigGreenBanana和一个a_big_green_banana。

CSS并不是真正的编程语言

每隔几个月就会出现一篇文章表明:CSS并不是真正的编程语言。以编程语言的标准来说,CSS过于困难。使用这门语言会很有创造性:事实确实如此,CSS不同于传统的编程,且具有缺陷,同任何标准化编程语言相比

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!