函数式编程

1.什么是函数式编程

函数式编程是一种思维方式,强调在编程过程中把更多的关注点放在如何去构建映射关系。

函数式编程和命令式编程区别:函数式编程关心数据的映射,命令式编程关心解决问题的步骤。

下面举个例子展示下两者区别:

假设我们需要进行如下结构转换:

 ['john-rose', ‘linda-luo ', ‘lucy-han']   
 // 转成 
 [{name: 'John Rose'}, {name: Linda Luo'}, {name: Lucy Han'}]

命令式编程

思路:

/*  1.定义一个临时变量 newArr。

  2.做一个循环,需要做 arr.length 次。

  3.循环每次把名字的首位取出来大写,然后拼接剩下的部分。

  ……

  4.最后返回结果。*/

//代码实现
const arr = ['john-rose', 'linda-luo', 'lucy-han'];
const newArr = [];
for (let i = 0, len = arr.length; i < len ; i++) {
  let name = arr[i];
  let names = name.split('-');
  let newName = [];
  for (let j = 0, naemLen = names.length; j < naemLen; j++) {
    let nameItem = names[j][0].toUpperCase() + names[j].slice(1);
    newName.push(nameItem);
  }
  newArr.push({ name : newName.join(' ') });
}
return newArr;

缺点:产生一堆中间临时变量,同时过程中掺杂了大量逻辑,可读性差、不易维护。通常一个函数需要从头读到尾才知道它具体做了什么,而且一旦出问题很难定位。

函数式编程

思路:

/*
1.将string数组转化为对象数组    [a, b, c]  ->  [{name: a’}, {name: b’}, {name: c’}]

2.将单个string转化为单个object( convert2Obj )   String -> Object    a -> {name: a’}

    a.将string转为为指定的string( capitalizeName )  String -> String    a –> a’

    b.将string转化为object( genObj ) String -> Object  a' -> {name: a’}
*/
// 代码实现
import * as R from 'ramda'
const {curry, compose, join, map, split} = R
const capitalize = x => x[0].toUpperCase() + x.slice(1).toLowerCase();

const genObj = curry((key, x) => {
  let obj = {};
  obj[key] = x;
  return obj;
}) 

const capitalizeName = compose(join(' '), map(capitalize), split('-'));
const convert2Obj = compose(genObj('name'), capitalizeName)
const convertName = map(convert2Obj);

convertName(['john-rose', 'linda-luo', 'lucy-han'])

特点: 着眼点是函数,而不是过程。强调的是如何通过函数的组合变换去解决问题

2. 函数式编程的特点

  • 函数是“一等公民”

这是函数式编程得以实现的前提,因为我们基本的操作都是在操作函数。这个特性意味着函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

  • 声明式编程

函数式编程大多时候都是在声明我需要做什么,而非怎么去做。这种编程风格称为声明式编程 。这样有个好处是代码的可读性特别高,同时也方便我们进行分工协作。

  • 函数惰性执行

所谓惰性执行指的是函数只在需要的时候执行,即不产生无意义的中间变量。函数式编程跟命令式编程最大的区别就在于几乎没有中间变量,它从头到尾都在写函数。

  • 无状态和数据不可变

数据不可变: 它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。

无状态: 主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。

  • 没有副作用

副作用指:在完成函数主要功能之外完成的其他副要功能。在我们函数中最主要的功能当然是根据输入返回结果,而在函数中我们最常见的副作用就是随意操纵外部变量, 如修改全局变量,修改入参等。

  • 纯函数

不依赖外部状态(无状态): 函数的的运行结果不依赖全局变量,this 指针,IO 操作等。

没有副作用(数据不变): 不修改全局变量,不修改入参。

所以纯函数才是真正意义上的 “函数”, 它意味着相同的输入,永远会得到相同的输出

纯函数的意义:

a. 便于测试和优化 b. 可缓存性 c. 更少的 Bug

3.流水线的构建(柯里化、函数组合)

函数式编程更为关注构建关系,数据可以不断的从一个函数的输出可以流入另一个函数输入,最后再输出结果。我们可以把上面函数式编程的过程抽象为流水线的构建,有两种操作是必不可少的那无疑就是柯里化(Currying)函数组合(Compose),柯里化其实就是流水线上的加工站,函数组合就是我们的流水线,它由多个加工站组成。

流水线1.png
流水线2.png

接下来,就让我们看看柯里化(Currying)函数组合(Compose)

加工站-函数柯里化

柯里化的意思是将一个多元函数,转换成一个依次调用的单元函数

f(a,b,c) → f(a)(b)(c)

为什么这个单元函数很重要?因为函数的返回值,有且只有一个, 如果我们想顺利的组装流水线,那我就必须保证我每个加工站的输出刚好能流向下个工作站的输入。因此,在流水线上的加工站必须都是单元函数。

  • 部分函数应用 vs 柯里化
// 柯里化 
 f(a,b,c) → f(a)(b)(c) 

// 部分函数调用
f(a,b,c) → f(a)(b,c) 、 f(a,b)(c)

部分函数( Partial Function Application )应用强调的是固定一定的参数,返回一个更小元的函数

柯里化强调的是生成单元函数部分函数应用的强调的固定任意元参数,而我们平时生活中常用的其实是部分函数应用,这样的好处是可以固定参数,降低函数通用性,提高函数的适合用性。

  • 高级柯里化

现成的函数库(如Ramda)提供的curry 函数,做了优化,不是纯粹的柯里化,可理解成高级柯里化。

实现可以根据你输入的参数个数,返回一个柯里化函数/结果值。即,如果你给的参数个数满足了函数条件,则返回值

我们可以用高级柯里化去实现部分函数应用,但是柯里化不等于部分函数应用

//代码演示
//简易实现
const curry = function(func) {
  return function curried(...args) {
    if (args.length < func.length) {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
    return func.apply(this, args);
  }
}

const add = R.curry((x, y, z) => x + y + z); 

const add7 = add(7); 
add7(1,2) // 10 

const add1_2 = add(1,2); 
add1_2(7) // 10 

add(7)(1)(2) // 10

流水线-函数组合

函数组合的目的是将多个函数组合成一个函数。下面来看一个简化版的实现:

const compose = (...fns) => 
    (...args) => 
            fns.reduceRight((val, fn) => fn.apply(null, [].concat(val)), args); 

const f = x => x + 1;
const g = x => x * 2; 
const t = (x, y) => x + y; 

let fgt = compose(f, g, t); 

fgt(1, 2); // 3 -> 6 -> 7

函数组合的好处

函数组合的好处显而易见,它让代码变得简单而富有可读性,同时通过不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力。

大型的程序,都可以通过一步步的拆分组合去实现,而剩下要做的,就是去构造足够多的积木块(函数)。

4.总结

优点:

  • 代码简洁,开发快速:

函数式编程大量使用函数的组合,函数的复用率很高,减少了代码的重复,因此程序比较短,开发速度较快。

  • 接近自然语言,易于理解:

函数式编程大量使用声明式代码,基本都是接近自然语言的,加上它没有乱七八糟的循环,判断的嵌套,因此特别易于理解。

  • 易于并发编程:

函数式编程没有副作用,所以函数式编程不需要考虑“死锁”(Deadlock),所以根本不存在“锁”线程的问题。

  • 更少的出错概率:

因为每个函数都很小,而且相同输入永远可以得到相同的输出,因此测试很简单,同时函数式编程强调使用纯函数,没有副作用,因此也很少出现奇怪的 Bug。

缺点:

  • 性能:

函数式编程往往会对一个方法进行过度包装,从而产生上下文切换的性能开销。

  • 资源占用:

在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式。这在某些场合会产生十分严重的问题。

  • 递归陷阱:

在函数式编程中,为了实现迭代,通常会采用递归操作。

总结:

因此,在性能要求很严格的场合,函数式编程其实并不是太合适的选择。

我们完全可以在日常工作中将函数式编程作为一种辅助手段,在条件允许的前提下,借鉴函数式编程中的思路,例如:多使用纯函数减少副作用的影响;使用柯里化增加函数适用率;使用它的编程风格,减少无意义的中间变量,让代码更具可读性。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容