函数式编程

184 阅读8分钟

什么是函数式编程

函数式编程(Functional Programming, FP),FP 是编程范式之一,我们常听说的编程范式还有面向过程 编程、面向对象编程。

  • 面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和 多态来演示事物事件的联系
  • 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽 象)
  • 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
  • x -> f(联系、映射) -> y,y=f(x)
  • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y= sin(x),x和y的关系
  • 相同的输入始终要得到相同的输出(纯函数)
  • 函数式编程用来描述数据(函数)之间的映射

函数式编程的前置知识

  1. 在JavaScript中,函数是一等公民
  2. 高阶函数(用来屏蔽细节,只关心目标),常用的高阶函数有:filter,map,forEach,every等 函数可以存储在变量中,可以当作参数传递,还能当作返回值
  3. 闭包(延长作用域链)
    闭包的概念:内部函数可以访问外部函数的变量和参数
    闭包的本质:函数在执行的时候会放在一个执行栈上,当函数执行完毕后会从栈移除,但是,堆上的作用域成员因为还被外部引用着,得不到释放,因为就可以访问到;
    闭包 (Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。

可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员

function fn2(){
  let num = 100;
}
// 正常情况下,执行完fn2,里面的变量num会释放掉
function fn2(){
  let num = 100;
  return function(){
    console.log(num)
  }
}
// 在上面函数中,返回了一个函数,而且在函数中还访问了原来函数内部的成员,就可以称为闭包

// test
const res = fn2()
res()
// res为外部函数,当外部函数对内部成员有引用的时候,那么内部的成员num就不能被释放。当调用res时,就可以访问num。

纯函数

纯函数概念

纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

纯函数的优点:

  • 可缓存:因为对于相同的输入始终有相同的结果,那么可以把纯函数的结果缓存起来,可以提高性能
  • 可测试:纯函数让测试更加的方便
  • 并行处理
  • 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
  • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)
//  调用lodash
const _ = require('lodash')
function getArea(r) {
  console.log(r)
  return Math.PI * r * r
}

let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 4
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669

// 看到输出的4只执行了一次,因为其结果被缓存下来了

下面模拟一个记忆函数

function memoize (f) {
  let cache = {}
  return function () {
    // arguments是一个伪数组,所以要进行字符串的转化
    let key = JSON.stringify(arguments)
    // 如果缓存中有值就把值赋值,没有值就调用f函数并且把参数传递给它
    cache[key] = cache[key] || f.apply(f,arguments)
    return cache[key]
  }
}

let getAreaWithMemory1 = memoize(getArea)
console.log(getAreaWithMemory1(4))
console.log(getAreaWithMemory1(4))
console.log(getAreaWithMemory1(4))
// 4
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669

副作用

纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部

的状态就无法保证输出相同,就会带来副作用。

副作用来源:

  • 配置文件
  • 数据库
  • 获取用户的输入
  • ......

所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作

用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控

范围内发生。

柯里化 (Haskell Brooks Curry)

柯里化 (Currying):

当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)

然后返回一个新的函数接收剩余的参数,返回结果。

将多变量函数拆解为单变量的多个函数的依次调用;
就是利用函数执行,可以形成一个不销毁的私有作用域,把预先处理的内容放到不销毁的作用域里面,返回一个函数供以后调用;

// 柯里化

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

// ES6 写法

let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
checkAge18(24)
checkAge18(20)

模拟lodash中的curry函数实现:

function curry (func) {
    return function curriedFn (...args) {
        // 判断实参和形参的个数
        if (args.length < func.length) {
            return function () {
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        // 实参和形参个数相同,调用 func,返回结果
        return func(...args)
    }
}

柯里化优点:

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 参数复用(对函数参数的‘缓存’)
  • 让函数粒度更细,变的更灵活
  • 将多元函数比变成一元函数,然后组合函数产生更强大功能

函数组合

纯函数和柯里化很容易写出洋葱代码 h(g(f(x))),函数组合可以避免这种情况;

a --> fn --> b
a-> f3 -> m -> f2 -> n -> f1 -> b
其实中间m、n、是什么我们也不关心 类似于下面的函数

先来看看Lodash中的组合函数用法

  • flow() //从左往右执行
  • flowRight() //从右往左执行

**

//  获取数组的最后一个元素并转化成大写字母
const _ = require('lodash')

const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()

const f = _.flowRight(toUpper, first, reverse)

console.log(f(['one', 'two', 'three'])) // THREE

简单实现一个flowRight函数
分析: 入参不固定,都是函数,出参是一个函数,这个函数要接受一个初始值

function compose(...args) {
  // args代表调用compose传入的要组合的函数数组
  return function (value) {
    // compose返回的函数接受一个初始值value
    // 因为要从右往左执行,所以数组反转一下
    // reduce方法接受两个参数:一个迭代函数,一个初始化值;
    // 其中的迭代函数的前两个参数:total代表上一次调用fn的返回值,fn指当前正在处理值(此处是函数)
    return args.reverse().reduce(function (total, fn) {
      return fn(total);
    }, value);
  };
}
// 完整版
const compose2 = (...funcs) => {
  // funcs(数组):记录的是所有的函数
  // 这里其实也是利用了柯里化的思想,函数执行,生成一个闭包,预先把一些信息存储,供下级上下文使用
  return (x) => {
    const len = funcs.length;
    // 如果没有函数执行,直接返回结果
    if (len === 0) return x;
    if (len === 1) funcs[0](x);
    return funcs.reduceRight((res, func) => {
      return func(res);
    }, x);
  };
};

// ES6实现

const compose3 = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)

var resFn = compose(c, b, a, a);
resFn(1);
//test
const reverse = (arr) => arr.reverse();
const first = (arr) => arr[0];
const toUpper = (s) => s.toUpperCase();

const fTest = compose(toUpper, first, reverse);
console.log(fTest(['one', 'two', 'three'])); // THREE

lodash/fp

  • lodash 的 fp 模块提供了实用的对函数式编程友好的方法

  • 提供了不可变 auto-curried iteratee-fifirst data-last 的方法

Point Free

Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参

数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。

  • 不需要指明处理的数据

  • 只需要合成运算过程

  • 需要定义一些辅助的基本运算函数

// 非 Point Free 模式

// Hello World => hello_world

function f (word) {

return word.toLowerCase().replace(/\s+/g, '_');

}

// Point Free

const fp = require('lodash/fp')

const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)

console.log(f('Hello World'))
  • 使用 Point Free 的模式,把单词中的首字母提取并转换成大写
const fp = require('lodash/fp')

const firstLetterToUpper = fp.flowRight(join('. '),

fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))

console.log(firstLetterToUpper('world wild web'))

// => W. W. W

中心思想

function first,data last

Functor函子

什么是 Functor

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

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

IO 函子

IO 函子中的 _value 是一个函数,这里是把函数作为值来处理

IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操

作纯

把不纯的操作交给调用者来处理



const fp = require('lodash/fp')
class IO {
  static of (x) {
    return new IO(function () {
      return x
    })
  }
  constructor (fn) {
    this._value = fn
  }
  map (fn) {
    // 把当前的 value 和 传入的 fn 组合成一个新的函数
    return new IO(fp.flowRight(fn, this._value))
  }
}
// 调用
let io = IO.of(process).map(p => p.execPath)

console.log(io._value())

Task 异步执行

  • 异步任务的实现过于复杂,我们使用 folktale 中的 Task 来演示

  • folktale 一个标准的函数式编程库

    • 和 lodash、ramda 不同的是,他没有提供很多功能函数

    • 只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、MayBe 等


const { compose, curry } = require('folktale/core/lambda')

const { toUpper, first } = require('lodash/fp')

// 第一个参数是传入函数的参数个数

let f = curry(2, function (x, y) {

console.log(x + y)

})

f(3, 4)

f(3)(4)

// 函数组合

let f = compose(toUpper, first)

f(['one', 'two'])

  • Task 异步执行

    • folktale(2.3.2) 2.x 中的 Task 和 1.0 中的 Task 区别很大,1.0 中的用法更接近我们现在演示的函子

    • 这里以 2.3.2 来演示

const { task } = require('folktale/concurrency/task')

function readFile(filename) {

    return task(resolver => {

        fs.readFile(filename, 'utf-8', (err, data) => {

            if (err) resolver.reject(err)

            resolver.resolve(data)

        })

    })

}

// 调用 run 执行

readFile('package.json')

  .map(split('\n'))

  .map(find(x => x.includes('version')))

  .run().listen({

    onRejected: err => {

      console.log(err)

    },

    onResolved: value => {

      console.log(value)

   }
 })

附录


函数式编程指北

函数式编程入门

Pointfree 编程风格指南

图解 Monad

Functors, Applicatives, And Monads In Pictures