函数式编程
什么是函数式编程
SQL 语句就是声明式的,你无需关心 Select 语句是如何实现的,不同的数据库会去实现它自己的方法并且优化。
所谓惰性执行指的是函数只在需要的时候执行,即不产生无意义的中间变量 函数式编程跟命令式编程最大的区别就在于几乎没有中间变量,它从头到尾都在写函数,只有在最后的时候才通过调用 convertName 产生实际的结果。
这是函数式编程的核心概念:
数据不可变: 它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
无状态: 主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。
对比这两段代码
const curUser = {
name: "Peter",
};
const saySth = (user, str) => user.name + ": " + str; // 不依赖外部变量
const changeName = (user, name) => ({ ...user, name }); // 未修改外部变量
const newUser = changeName(curUser, "Jay"); // { name: 'Jay' }
saySth(curUser, "hello!"); // Peter: hello!
const curUser = {
name: "Peter",
};
const saySth = (str) => curUser.name + ": " + str; // 引用了全局变量
const changeName = (obj, name) => (obj.name = name); // 修改了输入参数
changeName(curUser, "Jay"); // { name: 'Jay' }
saySth("hello!"); // Jay: hello!
柯里化的意思是将一个多元函数,转换成一个依次调用的单元函数。
部分函数应用强调的是固定一定的参数,返回一个更小元的函数。
// 柯里化
f(a,b,c) → f(a)(b)(c)
// 部分函数调用
f(a,b,c) → f(a)(b,c) / f(a,b)(c)
柯里化强调的是生成单元函数,部分函数应用的强调的固定任意元参数,而我们平时生活中常用的其实是部分函数应用,这样的好处是可以固定参数,降低函数通用性,提高函数的适合用性.
// 假设一个通用的请求 API
const request = (type, url, options) => ...
// GET 请求
request('GET', 'http://....')
// POST 请求
request('POST', 'http://....')
// 但是通过部分调用后,我们可以抽出特定 type 的 request
const get = request('GET');
get('http://', {..})
把要操作的数据放最后面
const split = curry((x, str) => str.split(x));
const join = curry((x, arr) => arr.join(x));
const replaceSpaceWithComma = compose(join(","), split(" "));
const replaceCommaWithDash = compose(join("-"), split(","));
函数组合的 debug 可以用 trace
const trace = curry((tip, x) => {
console.log(tip, x);
return x;
});
const lastUppder = compose(toUpperCase, head, trace("after reverse"), reverse);
// PointFree 没有出现需要操作的参数
const upperLastItem = compose(toUpperCase, head, reverse);
// 非 PointFree 出现了需要操作的参数
const upperLastItem = (arr) => {
const reverseArr = arr.reverse();
const head = reverseArr[0];
return head.toUpperCase();
};
const Box = (x) => ({
map: (f) => Box(f(x)),
inspect: () => `Box(${x})`,
});
const finalPrice = (str) =>
Box(str)
.map((x) => x * 2)
.map((x) => x * 0.8)
.map((x) => x - 50);
const result = finalPrice(100);
console.log(result); // => Box(110)
new 让人误以为是创建了 Class 的实例,但其实根本不存在所谓的实例化,只是简单的属性委托机制(对象组合的一种),而 this 则引入了执行上下文和词法作用域的问题,而我只是想创建一个简单的对象而已!
map 知道如何在上下文中映射函数值。它首先会打开该容器,然后把值通过函数映射为另外一个值,最后把结果值再次包裹到一个新的同类型的容器中。而这种变换容器内的值的方法(map)称为 Functor(函子)。
map 是把函数执行的结果重新包装到 Box 中后然返回一个新的 Box 类型,而 forEach 则是直接把函数执行的结果 return 出来,就结束了!
什么是 functor
上面我们定义了一个简单的 Box,其实也就是拥有 map 和 fold 方法的类型。让我们把脚步放慢一点,再仔细观察和思考一下我们的 map:Box(a) -> Box(b) ,本质上就是通过一个函数 a -> b 把一个 Box(a) 映射为 Box(b)。这和中学代数中的函数知识何其类似,不妨再回顾一下代数课本中函数的定义
假设 A 和 B 是两个集合,若按照某种对应法则,使得 A 的任一元素在 B 中都有唯一的元素和它对应,则称这种对应为从集合 A 到集合 B 的函数。
常见的 functor
Promise 的 then 和 catch 方法(Promise 也是一种 Functor? Yes!)。
Rxjs Observable 的 map 和 filter (异步函数的组合?Relax!)。
[1, 2, 3].map((x) => x + 1).filter((x) => x > 2);
$("#mybtn")
.css("width", "100px")
.css("height", "100px")
.css("background", "red");
Promise.resolve(1)
.then((x) => x + 1)
.then((x) => x.toString());
Rx.Observable.fromEvent($input, "keyup")
.map((e) => e.target.value)
.filter((text) => text.length > 0)
.debounceTime(100);
const partial =
(fn, ...presetArgs) =>
(...laterArgs) =>
fn(...presetArgs, ...laterArgs);
const double = (n) => n * 2;
const map = (fn, F) => F.map(fn);
const mapDouble = partial(map, double);
const res = mapDouble(Box(1)).fold((x) => x);
console.log(res); // => 2
const componse = (...fn) => x => (fn.reduceRight(x,v) => x + v)
先对比下功能强大的 box 理念, 也就是最基本的 functor,
函数式编程实践
数组的增删改查
const arr = [1, 2, 3, 4, 5];
const newArr = [...arr, 6];
const arr = [1, 2, 3, 4, 5];
const newArr = arr.filter((item) => item !== 3);
const arr = [1, 2, 3, 4, 5];
const newArr = arr.map((item) => (item === 3 ? 6 : item));
const arr = [1, 2, 3, 4, 5];
const newArr = arr.find((item) => item === 3);
对象的增删改查
const obj = { a: 1, b: 2 };
const newObj = { ...obj, c: 3 };
const obj = { a: 1, b: 2 };
const { a, ...newObj } = obj;
const obj = { a: 1, b: 2 };
const newObj = { ...obj, a: 3 };
const obj = { a: 1, b: 2 };
const a = obj.a;
数组和对象的组合
const arr = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 3, name: "c" },
];
const newArr = [...arr, { id: 4, name: "d" }];
const arr = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 3, name: "c" },
];
const newArr = arr.filter((item) => item.id !== 2);
const arr = [
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 3, name: "c" },
];
const newArr = arr.map((item) =>
item.id === 2 ? { ...item, name: "d" } : item
);
js 改变原数组和不改变原数组的方法
改变原数组的方法
let a = arr.push("f");
console.log(a); // 5 返回数组长度
console.log(arr); // ['a', 'b', 'c', 'd', 'f']
let a = arr.pop();
console.log(a); // d
console.log(arr); // ['a', 'b', 'c']
let a = arr.shift();
console.log(a); // a
console.log(arr); // ['b', 'c', 'd']
unshift 在数组的开头添加一个或更多元素,并返回新的长度
let a = arr.unshift(0);
console.log(a); // 5 返回数组长度
console.log(arr); // [0, 'a', 'b', 'c', 'd']
let a = arr.splice(1, 2, "f");
console.log(a); // 返回被删除的元素数组['b', 'c']
console.log(arr); // 在添加的地方添加元素后的数组["a", "f", "d"]
let arr = ["c", "a", "d", "b"];
let a = arr.sort();
console.log(a); // ['a', 'b', 'c', 'd']
console.log(arr); // ['a', 'b', 'c', 'd']
let a = arr.reverse();
console.log(a); // ["d", "c", "b", "a"]
console.log(arr); // ["d", "c", "b", "a"]
let a = arr.fill("e", 2, 4);
console.log(a); // 返回它会改变调用它的 `this` 对象本身, 然后返回它['a', 'b', 'e', 'e']
console.log(arr); // ['a', 'b', 'e', 'e']
copyWithin 从数组的指定位置拷贝元素到数组的另一个指定位置中
let a = arr.copyWithin(1, 2, 3);
console.log(a); //返回被复制的元素数组 ['a', 'c', 'c', 'd']
console.log(arr); //原元素数组已经改变 ['a', 'c', 'c', 'd']
map: 只有当 arr 为基本数据类型时,map 方法才不会改变原始数组,arr 为引用类型时,还是会改变原数组
let arr = [{ a: 1 }, { b: 2 }];
let a = arr.map((item) => (item.a = 2));
console.log(a); // [2, undefined]
console.log(arr); // [{a: 2}, {b: 2}]
不改变原数组的方法
let a = arr.concat(["e", "f"]);
console.log(a); // 返回一个新数组 ['a', 'b', 'c', 'd', 'e', 'f']
console.log(arr); // 原数组不变 ['a', 'b', 'c', 'd']
let a = arr.slice(1);
console.log(a); // ["b", "c", "d"]
console.log(arr); // ["a", "b", "c", "d"] 不变
slice(-1); // ["d"]
join 将数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串
let a = arr.join("-");
console.log(a); // "a-b-c-d"
console.log(arr); // ["a", "b", "c", "d"] 不变
let a = arr.filter((item) => item !== "b");
console.log(a); // ["a", "c", "d"]
console.log(arr); // ["a", "b", "c", "d"] 不变
reduce 对数组中的每个元素执行一个由您提供的“reducer”函数(升序执行),将其结果汇总为单个返回值
let a = arr.reduce((pre, cur) => pre + cur);
console.log(a); // "abcd"
console.log(arr); // ["a", "b", "c", "d"] 不变
every 对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true
let a = arr.every((item) => item === "a");
console.log(a); // false
console.log(arr); // ["a", "b", "c", "d"] 不变
some 对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true
let a = arr.some((item) => item === "a");
console.log(a); // true
console.log(arr); // ["a", "b", "c", "d"] 不变
find 返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined
let a = arr.find((item) => item === "a");
console.log(a); // "a"
console.log(arr); // ["a", "b", "c", "d"] 不变
findIndex 返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1
let a = arr.findIndex((item) => item === "a");
console.log(a); // 0
console.log(arr); // ["a", "b", "c", "d"] 不变