me

函数柯里化、偏函数以及惰性函数


柯里化

Note

什么是函数柯里化?

在计算机中,柯里化是将使用多个参数的一个函数转换成一些列使用一个参数的函数

function add(a: number, b: number) {
  return a + b;
}
//执行 add 函数,依次传入两个参数
add(1, 2);

//如果有一个 carry 函数,可以做到柯里化
let addCurry = curry(add);
addCurry(1)(2);

柯里化的用途可以理解为参数复用,本质上是降低通用性,提高适用性

const person = [{ name: "zhangsan" }, { name: "lisi" }];
function curry<T>(fn: Function): Function {
  return function (...args: T[]) {
    if (args.length < fn.length) {
      return curry(fn.bind(this, ...args));
    } else {
      return fn(...args);
    }
  };
}

function add(a: number, b: number, c: number) {
  return a + b + c;
}

let curryAdd = curry(add);
console.log(curryAdd(1, 3)(3));
type IPerson = {
  name: string;
  age: number;
};

const person: IPerson[] = [
  { name: "zhangsan", age: 12 },
  { name: "lisi", age: 100 },
];

let prop = curry((key: keyof IPerson, obj: IPerson) => {
  return obj[key];
});

console.log(person.map(prop("name")));

偏函数 (Partial Function)

在计算机科学中,偏函数是指固定一个函数的一些参数,然后产生另一个更小元的函数。

什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。

function add(a, b) {
  return a + b;
}

// 执行 add 函数,一次传入两个参数即可
add(1, 2); // 3

// 假设有一个 partial 函数可以做到局部应用
const addOne = partial(add, 1);

addOne(2); // 3

和函数柯里化的区别

当然也可以使用bind函数来实现偏函数

function test(a: number, b: number, c: number) {
  return a + b + c;
}

let bindOne = test.bind(null, 1);
console.log(bindOne(2, 3));

手动实现

function partical(fn: Function, ...args: any[]): Function {
  return function (...moreArgs: any[]) {
    return fn(...args, ...moreArgs);
  };
}

惰性函数

惰性函数就是说函数执行一次后,之后调用函数都是相同的作用,直接返回第一次执行函数。很多时候只需要执行一次,因为之后每次调用函数执行的结果都一样。所以如果函数语句不必每次执行,我们可以使用称之为惰性函数的技巧来进行优化。

惰性求值 (Lazy evaluation)

按需求值机制,只有当需要计算所得值时才会计算

const rand = function* () {
  while (true) {
    yield Math.random();
  }
};

const randIter = rand();
randIter.next();

纯函数 (Purity)

//纯函数
const greet = (name: string) => `hello, ${name}`;
greet("world");

//不是纯函数,修改了外部的状态
let greeting: string;
const greet = (name: string) => {
  greeting = `hello, ${name}`;
};
greet("world");

副作用 (Side effects):如果函数与外部可变状态进行交互,则它就是具有副作用的

//Date 对象就是一个很常见的具有副作用的函数
const differentEveryTime = new Date();

幂等性

幂等性和纯函数还是有很大区别的,甚至说可以说是毫无无关系

Math.abs(Math.abs(-10));

函数组合 (Function Composing)

接收多个函数作为参数,从右到左,一个函数的输入为以一个函数的输出

const compose =
  (f: Function, g: Function): Function => (a: Function): Function => f(g(a));
//不能确定返回索引的类型,可以使用泛型,这里就用 any 了
function first<T>(arr: T[]): any {
  return arr[0];
}
function reverse<T>(arr: T[]): T[] {
  return arr.reverse();
}
let last = compose(first, reverse);
console.log(last([1, 2, 3, 4, 5]));

当然 redux 给出了一个更好的实现 (将函数的个数情况也考虑周全了)

function compose(...fns: Function[]) {
  if (fns.length === 0) {
    return (arg: Function) => arg;
  }
  if (fns.length === 1) {
    return fns[0];
  }
  return fns.reduce(
    (a, b) =>
    //Writing a type here won't do any good
    <T>(...args: T[]): Function => a(b(...args)),
  );
}

生成器的方式

实现 map 映射函数,由于生成器的yield接受的是上一次的结果,所以第一次的迭代效果是无效的

function* genMap(iteratee: Function): Generator<string | null, any> {
  let input = yield null;
  while (true) {
    input = yield iteratee(input);
  }
}

const gen = genMap((x: string) => x.toUpperCase());
const arr = ["a", "b", "c"];
console.log(gen.next());
for (let i of arr) {
  console.log(gen.next(i));
}
function* genMap(
  iterable: Iterable<any>,
  iteratee: Function,
): Generator<string | null, any> {
  for (let i of iterable) {
    yield iteratee(i);
  }
}
const gen = genMap(["a", "b", "c"], (x: string) => x.toUpperCase());

使用 yield* 来调用另一个生成器的方式来进行函数组合, iterable会不停的叠加作用域

function* genCompose(
  iterable: Iterable<any>,
  ...fns: Function[]
): Generator<any, any, any> {
  for (let fn of fns) {
    iterable = genMap(iterable, fn);
  }
  yield* iterable;
}

const composed = genCompose(
  [1, 2, 3],
  (x: number) => x + 1,
  (x: number) => x * x,
  (x: number) => x - 2,
);

Pointfree

这是函数式编程的答案,利用函数组合和柯里化可以达到一个很好的函数式效果

//ramda
fn = R.pipe(f1, f2, f3);

Pointfree的本质就是使用一些通用的函数,组合除各种复杂的运算.shang 层运算不直接操作数据

interface Iperson {
  name: string;
  role: string;
}

const data: Iperson[] = [
  { name: "张三", role: "worker" },
  { name: "李四", role: "worker" },
  { name: "王五", role: "manager" },
];

type Iper = keyof Iperson;

const isWorker = (s: string) => s === "worker";
//定义查找角色的函数,在这里嵌套会增加耦合
const prop = (p: Iper, obj: Iperson) => isWorker(obj[p]);
//指定读取 role 的值
const propRole = curry(prop)("role");

data.filter(propRole);
const prop = (p: Iper, obj: Iperson) => obj[p];
console.log(
  data.filter((_, index) => compose(isWorker, propRole)(data[index])),
);

函数记忆

只要把参数和对应的结果数据存到一个对象中,调用时,判断参数对应的数据是否存在,存在就返回对应的值

const memoize = function (fn: Function, hasher?: Function) {
  let cache: any = {};
  const menoize = function (...args: any[]) {
    const address = "" + (hasher ? hasher.apply(null, args) : args);
    if (!cache[address]) {
      cache[address] = fn.apply(null, args);
    }
    return cache[address];
  };
  return menoize;
};

当然,抄袭underscore的使用 ts 重够了一下很快

let add = function (a: number, b: number, c: number) {
  return a + b + c;
};
let memoizedAdd = memoize(add);
console.log(memoizedAdd(1, 2, 3));

测试一下是,没有问题的。但是我们如果想要使用一个生成键的函数hasher

const memoizedAdd = memoize(add, function () {
  const args = Array.prototype.slice.call(arguments, 0, 1);
  return JSON.stringify(args);
});