【JS】浅谈FP

背景

当需要实现两个功能函数,一个transform1 : input 一个字符串,output 要全部转成大写并尾部加感叹号修饰,一个transform2 : input 一个字符串,output 要全部转成小写并尾部加感叹号修饰。以前的思维通常会这样实现:

JavaScript 复制代码
const transform1 = (str) => {
  if (typeof str === "string") {
    return `${str.toUpperCase()}!`;
  }
  return "Not a string";
};

const transform2 = (str) => {
  if (typeof str === "string") {
    return `${str.toLowerCase()}!`;
  }
  return "Not a string";
};
transform1("hello world"); // "HELLO WORLD !"
transform2("HELLO WORLD"); // "hello world !"

可以发现这种写法有很多重复的地方并且功能复用性很差。而FP 精华就是会尽量抽象化并拆分成最小单位。一开始也许会觉得多余,但FP 中每一个function 都是可以再利用的。FP的写法如下:

JavaScript 复制代码
const { flow } = require("lodash/fp");
const toUpper = (str) => str.toUpperCase();
const toLower = (str) => str.toLowerCase();
const exclaim = (str) => `${str}!`;
const isString = (str) => (typeof str === "string" ? str : "Not a string");
const transform1 = flow(isString, toUpper, exclaim);
const transform2 = flow(isString, toLower, exclaim);
transform1("hello world"); // "HELLO WORLD !"
transform2("HELLO WORLD"); // "hello world !"

开始可能觉得没什么必要,但是在中大型项目里尤其好用,因为我们也不知道未来需求会变得多复杂。FP 使用大量的Function,每个function都是一个单一的功能,再按功能需求以特定的方式组合起来,编写时易于复用,在出现bug时也易于快速定位到相关的功能函数,使得代码减少重复、容易理解、容易改变、容易排除错误和具有弹性。

介绍

FP(Functional Programming)是一种通过简单地组合一组函数来编写程序的风格。本质上,FP 要求我们将几乎所有东西都包装在函数中,编写大量可重用的小函数,然后简单地一个接一个地调用它们以获得类似的结果:( func1.func2.func3 ) 或以组合方式,例如:func1(func2 (func3()))。总而言之是:一种抽象思维、一种编程风格、一种编程规范。通常FP具有以下特点:

Function 为First-class citizen(一等公民)

这个特性意味着函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值,而js 的function 本来就有这个特性。这也是FP得以实现的前提。

Declarative Programming(声明式编程)

FP是Declarative Programming 的代表,逻辑为用较为抽象的程式码,理解代码想要达到怎样的目标,Function 之间不会互相共用state 状态(着重what)。而Imperative Programming (命令式编程)比较容易写出状态互相依赖的代码(着重how)。举个加总Array🌰:

JavaScript 复制代码
//Imperative 着重how一步步得出结果
var array = [3,2,1]
var total = 0
for( var i = 0 ; i <= array.length ; i ++ ){
    total += array[i]
}

//OOP
//通过封装把状态(数据)和行为(方法)内聚到类中,对外提供能够访问或操作状态的方法。
class Total {
    constructor(numbers = []) {
        this._numbers = numbers;
    }
    calc() {
        const numbers = this._numbers;
        let totle = 0;
        for (let i = 0; i < numbers.length; i++) {
            totle += numbers[i]
        }
        return totle;
    }
}

const totle1 = new Totle([1, 2, 3, 4, 5]);
console.log(totle1.calc());//15
JavaScript 复制代码
//Declarative 直接知道通过reduce做了什么,不关心reduce是怎样做的
var array = [3,2,1]
array.reduce( function( previous, current ){ 
    return previous + current 
})

没有Side Effect(副作用)

Side Effect:在完成函数主要功能之外完成的其他副要功能。 会导致不易维护代码、得到无法预期结果等等。而平常撰写javaScript 容易造成的Side Effect 非常之多,常见的包括:

  • 修改外部的state
JavaScript 复制代码
// 改了 global 变量
var a = 0;
a++;

const list = [{type:'香蕉',age:18},...];
// 修改 list 中的 type 和 age
list.map(item => {
  item.type = 1;
  item.age++;
})
  • 发送HTTP Request

  • Rendering screen

  • 使用会改变原数组/变量的JS method (eg. splice)

  • 修改任何外部变量

  • DOM 操作

  • 读取input 的值

  • Changing DB value

  • logging & console: 改变了系统状态

Immutable data

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

JavaScript 复制代码
// mutable
const balls = ['basketball', 'volleyball', 'billiards']
balls[1] = 'Table Tennis'; // 改变数组原有项
balls // ['Table Tennis', 'volleyball', 'billiards']

// immutable
const balls = ['basketball', 'volleyball', 'billiards']
const newBalls = [...balls] // 复制一份
newBalls[1] = 'Table Tennis'; 
balls  // ['basketball', 'volleyball', 'billiards'] 跟原本一样
newBalls// ['Table Tennis', 'volleyball', 'billiards']

Stateless

对于一个函数,完全不依赖外部状态的变化

JavaScript 复制代码
// Stateful
const x = 4; 
x++; // x 变 5
// 省略 100 行...

x*2 // ??x是啥都忘了

//Stateless 不用担心x是什么
const x = {
  val: 0
};
const x1 = x => { val: x.val + 1};

Pure Function

遵守one input, one output 原则,不管输入几次同样值,输出结果永远相同,且永远有输出值。只做运算与返回return,而且不对外部世界造成任何改变( 没有Side Effect)。Pure Function 里面data 多是immutable data 与stateless 的。另外当一个函数是pure function 且不依赖任何外部状态只依赖函数参数,也称作referential transparency (引用透明)。

JavaScript 复制代码
//impure 有side effect
const add = (x, y) => {
    console.log(`Adding ${x} ${y}`)
    return x + y
}
//pure
const add = (x, y) => {
    return {result: x + y, log: `Adding ${x} ${y}`}
}
//impure  当 n=4 没有返回值
function tll(i){
  if(i<3){
     return 0
  }
  if(i>5){
     return 1
  }
}
//pure
function tll(i){
  if(i<3){
     return 0
  }else{
     return 1
  }
}

//impure  相同输入返回值不一样
let x = 1
const count = ()=>x++
//pure
const count = (x)=>x+1

柯理化拆分,Composition合成

柯理化的意义是将具有多个参数的多元函数转化为具有较少参数的单元函数的过程。简单的柯理化函数是这样的:

JavaScript 复制代码
const curry = (fn, length = fn.length, ...args) =>
  args.length >= length ? fn(...args) : curry.bind(null, fn, length, ...args);

柯理化的作用是可以固定参数,降低函数通用性,提高函数的适合用性。举个🌰:

JavaScript 复制代码
// 假设一个通用的请求 APIconst request = (type, url, options) => ...
// GET 请求
request('GET', 'http://....')
// POST 请求
request('POST', 'http://....')

// 但是通过柯理化,我们可以抽出特定 type 的 request
const get = request('GET');
get('http://', {..})

Composition思维一般有两种实现形式,一是compose:(fa,fb,fc)=>x=>fa(fb(fc(x))),一是pipe:(fa,fb,fc)=>x=>fc(fb(fa(x)))

compose函数的简单实现:

JavaScript 复制代码
function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

pipe函数的实现同上不过改变一下执行顺序而已:

JavaScript 复制代码
function pipe(...funcs) {
  if (funcs.length === 0) {
    return arg => arg;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  return funcs.reduce((a, b) => (...args) => b(a(...args)));
}

举一个lodash与lodash/fp的🌰介绍柯理化与Composition组合的意义:

JavaScript 复制代码
//lodash实现对请求数据的处理 =>套娃(无柯理化)
const getIncompleteTaskSummaries = async function (memberName) {
  let data = await fetchData();
  return sortBy(
    map(reject(filter(get(data, "tasks"), "username"), "complete"), (task) =>
      pick(task, ["id", "dueDate", "title", "priority"])
    ),
    "dueDate"
  );
};

//lodash/fp 对FP有着更好的支持,包括完全柯理化、data-last等
const getIncompleteTaskSummaries = async function (memberName) {
  let data = await fetchData();
  return compose(
    sortBy("dueDate"),
    map(pick(["id", "dueDate", "title", "priority"])),
    reject("complete"),
    filter("username"),
    get("tasks")
  )(data);
};

可以看到经过柯理化拆分提高函数适用性后,通过函数组合使得代码如此的流畅、简洁。通过柯理化拆分和函数组合可以使得FP发挥很大的效用,也是FP必不可少的两步操作,可以将柯理化后的函数比作加工站,函数组合比作流水线。

相关内容

lodash/fp

lodash/fp官方介绍

ramda

ramda官方文档

对比:

lodash/fp、ramda都具备data-last、完全柯理化、组合函数、pure纯函数等利于FP的特点。但相比之下两者也有些差异:

  1. lodash/fp依赖于lodash,是在lodash基础上实现的对函数式编程的倾斜,好上手,但是受限于lodash,有很多局限性。ramda没有前置依靠,完全FP,整个库贯穿FP思想,但是上手成本高。

  2. ramda具备很多逻辑判断的函数(when,ifElse等),而lodash/fp暂无。

  3. ramda有更友善的文档,lodash/fp更多要与lodash进行对照。

相关推荐
纳尼亚awsl33 分钟前
无限滚动组件封装(vue+vant)
前端·javascript·vue.js
八了个戒38 分钟前
【TypeScript入坑】TypeScript 的复杂类型「Interface 接口、class类、Enum枚举、Generics泛型、类型断言」
开发语言·前端·javascript·面试·typescript
蓝莓味柯基1 小时前
React——点击事件函数调用问题
前端·javascript·react.js
一嘴一个橘子1 小时前
js 将二进制文件流,下载为excel文件
javascript
小兔崽子去哪了2 小时前
Element plus 图片手动上传与回显
前端·javascript·vue.js
A阳俊yi2 小时前
Vue(13)——router-link
前端·javascript·vue.js
清灵xmf3 小时前
提前解锁 Vue 3.5 的新特性
前端·javascript·vue.js·vue3.5
Jiaberrr3 小时前
教你如何在微信小程序中轻松实现人脸识别功能
javascript·微信小程序·小程序·人脸识别·百度ai
白云~️3 小时前
监听html元素是否被删除,删除之后重新生成被删除的元素
前端·javascript·html
2401_864476933 小时前
无线领夹麦克风哪个降噪好?一文搞懂麦克风什么牌子的音质效果好
javascript·git·sql·github·mssql