函数式编程简介

发布时间:2021-01-31

定义

简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。
它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。

FP和指令式编程相比,函数式编程的思维方式更加注重函数的计算。它的主要思想是把问题的解决方案写成一系列嵌套的函数调用。

就像在OOP中,一切皆是对象,编程的是由对象交合创造的世界;在FP中,一切皆是函数,编程的世界是由函数交合创造的世界。

历史背景

函数式编程中最古老的例子莫过于1958年被创造出来的Lisp了。Lisp由约翰·麦卡锡(John McCarthy,1927-2011)在1958年基于λ演算所创造,采用抽象数据列表与递归作符号演算来衍生人工智能。较现代的例子包括Haskell、ML、Erlang等。现代的编程语言对函数式编程都做了不同程度的支持,例如:JavaScript, Coffee Script,PHP,Perl,Python, Ruby, C# , Java 等等(这将是一个不断增长的列表)

特点

1、函数是"第一等公民"

对于JavaScript来说,函数可以赋值给变量,也可以作为函数参数,还可以作为函数返回值,因此JavaScript中函数是一等公民。

var name = 'jack'

function sayHi(name) {}

var hello = sayHi

var hi = () => sayHi

function ajax(cb) {
  cb()
}

2、没有"副作用"

所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

副作用让一个函数变得不纯是有道理的:从定义上来说,纯函数必须要能够根据相同的输入返回相同的输出;如果函数需要跟外部事物打交道,那么就无法保证这一点。 「相同输入得到相同输出」 function-sets.gif. relation-not-function.gif

slice 与 splice

var xs = [1,2,3,4,5];

// 纯的
xs.slice(0,3);
//=> [1,2,3]


// 不纯的
xs.splice(0,3);
//=> [1,2,3]

splice改变了原数组,使得相互引入共同的变量的地方潜藏着危险。slice不会改变原变量,接近于纯函数。

3、可测试性

纯函数让测试更加容易。我们不需要伪造一个“真实的”支付网关,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。

应用

1、函数缓存

纯函数总能够根据输入来做缓存。实现缓存的一种典型方式是 缓存 技术:


var squareNumber  = cache(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16

cache实现

function cache(fn) {
  var cache = Object.create(null);
  return function() {
    var args = Array.prototype.slice.call(arguments);
     //通过拼接参数形成一个独一无二的键值对key 
    var str = args.join("-");
      // 当有缓存的时候直接取缓存的,没缓存则只需执行fn函数进行处理并缓存
      return cache[str] || (cache[str] = fn.apply(fn, arguments));
  };
}

2、函数柯里化

把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

参数复用

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

3、函数组合

如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)。

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);

shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"

bg2017022209.png


compose(f, compose(g, h))
// 等同于
compose(compose(f, g), h)
// 等同于
compose(f, g, h)

场景 输入一个数字对其进行 +10 *10 +2

// 1
const add10 = num => num + 10
// 2
const multiply10 = num => num * 10
// 3
const add2 = num => num + 2

const disposeInput = input => add2(multiply10(add10(input)))

let num = disposeInput(2)
console.log('num', num)

// 函数组合 compose

const compose = (...fns) => x => fns.reduce((v, f) => f(v), x)

const dispose1 = compose(add10, multiply10, add2)

let num1 = dispose1(2)
console.log('num1', num1)

从上面的例子,我们看到disposeInput函数嵌套的写法让人抓狂,函数从内向外执行,如果真的用到我们的业务代码中,那将是一块难以维护的一块代码,我们实现一个从左到右执行到函数compose, 而下面经过组合的函数dispose1则显的清晰的多,而且更容易维护。组合函数可不止给人这种清晰易维护的感觉,下面的代码示例则展示如何追踪代码执行的结果。

对组合的函数进行追踪

const dispose2 = compose(
  trace('input'),
  add10,
  trace('add10'),
  multiply10,
  add2,
  trace('end')
)
let num2 = dispose2(5)
console.log('num2', num2)

// == input:  5
// == add10:  15
// == end:  152
// num2 152

从上面代码执行的结果看到,我们追踪某个函数的执行结果是没有侵入性的,不需要更改原函数的任何代码。我们定义到组合函数就像管道一样,把输入的值按顺序经过一个个管道进行处理并流向下一个管道。我们可以追踪到每个管道处理的结果,这就是组合函数的一个优势。

参考资料