函数式编程


1. 概念

函数式编程是一种强调以函数使用为主的软件开发风格。

函数式编程的目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用减少对状态的改变

2. 例子

(1)我们要显示一个hello world的文本

初学者:

document.querySelector('#msg').innerHTML = '<h1>Hello World</h1>'

有经验者:

function printMessage(elementId, format, message) {
    document.querySelector(elementId).innerHTML = `<${format}>${message}</${format}>`
}

printMessage('msg', 'h1', 'Hello World')

函数式开发者:

const printMessage = compose(addToDom('msg'), h1, echo)
printMessage('Hello World')

compose 和 addToDom 、h1 、echo 都是函数其中compose代码如下

function compose(...fns) {
    let len = fns.length
    let res = null
    return function fn(...arg) {
        res = fns[len - 1].apply(null, arg) // 每次函数运行的结果
        if(len > 1) {
            len --
            return fn.call(null, res) // 将结果递归传给下一个函数
        } else {
            return res //返回结果
        }
    }
}

函数式开发者将一个任务拆分成多个最小颗粒的函数,然后通过组合的方式来完成我们的任务,这跟我们组件化的思想很类似,将整个页面拆分成若干个组件,然后拼装起来完成我们的整个页面。在函数式编程里面,组合是一个非常非常非常重要的思想。

(2)函数式更改需求

我们现在再改变一下需求,现在我们需要将文本重复三遍,打印到控制台。

var printMessaage = compose(console.log, repeat(3), echo)

printMessage(‘Hello World’)

如此,更改了需求却没有修改内部逻辑,只是重组函数。

3. 相关概念

到函数式编程在开发中具有声明模式。为了充分理解函数式编程,我们先来看下几个基本概念。

  • 声明式编程
  • 纯函数
  • 引用透明
  • 不可变性

3.1 声明式编程

函数式编程属于声明是编程范式:这种范式会描述一系列的操作,但并不会暴露它们是如何实现的或是数据流如何传过它们。

我们所熟知的 SQL 语句就是一种很典型的声明式编程,它由一个个描述查询结果应该是什么样的断言组成,对数据检索的内部机制进行了抽象。

我们再来看一组代码再来对比一下命令式编程和声明式编程。

// 命令式方式
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
    array[i] = Math.pow(array[i], 2)
}

array; // [0, 1, 4, 9]

// 声明式方式
[0, 1, 2, 3].map(num => Math.pow(num, 2))

为什么我们要去掉代码循环呢?循环是一种重要的命令控制结构,但很难重用,并且很难插入其他操作中。而函数式编程旨在尽可能的提高代码的无状态性和不变性。要做到这一点,就要学会使用无副作用的函数–也称纯函数

3.2 纯函数

纯函数指没有副作用的函数。相同的输入有相同的输出,就跟我们上学的函数一样。

常常这些情况会产生副作用。

  • 改变一个全局的变量、属性或数据结构
  • 改变一个函数参数的原始值
  • 处理用户输入
  • 抛出一个异常
  • 屏幕打印或记录日志
  • 查询 HTML 文档,浏览器的 Cookie 或访问数据库

举一个简单的例子

var counter = 0
function increment() {
    return ++counter;
}

这个函数就是不纯的,它读取了外部的变量,可能会觉得这段代码没有什么问题,但是我们要知道这种依赖外部变量来进行的计算,计算结果很难预测,你也有可能在其他地方修改了 counter 的值,导致你 increment 出来的值不是你预期的。

对于纯函数有以下性质:

  • 仅取决于提供的输入,而不依赖于任何在函数求值或调用间隔时可能变化的隐藏状态和外部状态。
  • 不会造成超出作用域的变化,例如修改全局变量或引用传递的参数。

但是在我们平时的开发中,有一些副作用是难以避免的,与外部的存储系统或 DOM 交互等,但是我们可以通过将其从主逻辑中分离出来,使他们易于管理。

现在我们有一个小需求:通过 id 找到学生的记录并渲染在浏览器(在写程序的时候要想到可能也会写到控制台,数据库或者文件,所以要想如何让自己的代码能重用)中。

// 命令式代码

function showStudent(id) {
    // 这里假如是同步查询
    var student = db.get(id)
    if(student !== null) {
          // 读取外部的 elementId
          document.querySelector(`${elementId}`).innerHTML = `${student.id},${student.name},${student.lastname}`
    } else {
        throw new Error('not found')
    }
}

showStudent('666')

// 函数式代码

//curry函数
function curry(fn){
    //固定参数
    let fixed_args = Array.prototype.slice.call(arguments,1);
    return function(){
        let args = Array.prototype.slice.call(arguments)||[];
        args = fixed_args.concat(args);
        return fn.apply(this,args);
    }
}

// 通过 find 函数找到学生
var find = curry(function(db, id) {
    var obj = db.get(id)
    if(obj === null) {
        throw new Error('not fount')
    }
    
    return obj
})

// 将学生对象 format
var csv = (student) => `${student.id},${student.name},${student.lastname}`

// 在屏幕上显示
var append = curry(function(elementId, info) {
    document.querySelector(elementId).innerHTML = info
})

var showStudent = compose(append('#student-info'), csv, find(db))

showStudent('666')

curry (柯里化)是一个对于新手来说比较难理解的一个概念,在函数式编程里面起着至关重要的作用。

可以看到函数式代码通过较少这些函数的长度,将 showStudent 编写为小函数的组合。这个程序还不够完美,但是已经可以展现出相比于命令式的很多优势了。

  • 灵活。有三个可重用的组件
  • 声明式的风格,给高阶步骤提供了一个清晰视图,增强了代码的可读性
  • 另外是将纯函数与不纯的行为分离出来。

我们看到纯函数的输出结果是一致的,可预测的,相同的输入会有相同的返回值,这个其实也被称为引用透明

3.3 引用透明

引用透明是定义一个纯函数较为正确的方法。纯度在这个意义上表示一个函数的参数和返回值之间映射的纯的关系。如果一个函数对于相同的输入始终产生相同的结果,那么我们就说它是引用透明。

这个概念很容易理解,简单的举两个例子就行了。

// 非引用透明
var counter = 0

function increment() {
    return ++counter
}

// 引用透明
var increment = (counter) => counter + 1

其实对于箭头函数在函数式编程里面有一个高大上的名字,叫 lambda 表达式,对于这种匿名函数在学术上就是叫 lambda 表达式,现在在 Java 里面也是支持的。

3.4 不可变数据

不可变数据是指那些创建后不能更改的数据。与许多其他语言一样,JavaScript 里有一些基本类型(String,Number 等)从本质上是不可变的,但是对象就是在任意的地方可变。

考虑一个简单的数组排序代码:

var sortDesc = function(arr) {
    return arr.sort(function(a, b) {
        return a - b
    })
}

var arr = [1, 3, 2]
sortDesc(arr) // [1, 2, 3]
arr // [1, 2, 3]

这段代码看似没什么问题,但是会导致在排序的过程中会产生副作用,修改了原始引用,可以看到原始的 arr 变成了 [1, 2, 3]。这是一个语言缺陷,后面会介绍如何克服。

4. 总结

  • 使用纯函数的代码绝不会更改或破坏全局状态,有助于提高代码的可测试性和可维护性
  • 函数式编程采用声明式的风格,易于推理,提高代码的可读性。
  • 函数式编程将函数视为积木,通过一等高阶函数来提高代码的模块化和可重用性。
  • 可以利用响应式编程组合各个函数来降低事件驱动程序的复杂性(这点后面可能会单独拿一篇来进行讲解)。

参考:https://juejin.cn/post/6844903743117361165


文章作者: iamfugui
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 iamfugui !
评论