【JavaScript】JS 函数式编程入门指南:从概念到实践 (二)

411 阅读48分钟

本篇接上一篇【JavaScript】JS 函数式编程入门指南:从概念到实践 (一)~,继续介绍JS函数式编程的相关概念和实践。

1、Lambda

Lambda 是函数式编程中的一个重要概念,也称为匿名函数或箭头函数。它可以将一个函数作为值来传递或返回,从而能够更加灵活地处理函数。在 JavaScript 中,我们可以使用箭头函数来实现 Lambda。

下面是一个简单的示例代码:

// 一个普通的函数
function add(a, b) {
  return a + b;
}

// 使用 Lambda 表达式实现相同的功能
const addLambda = (a, b) => a + b;

// 调用这两个函数
console.log(add(3, 4)); // 输出:7
console.log(addLambda(3, 4)); // 输出:7

在上述代码中,我们定义了一个普通的函数 add(),它接受两个数字作为参数,并返回它们的和。然后,我们使用 Lambda 表达式实现了相同的功能,只不过它更加简洁,同时也更加灵活,可以很容易地被组合和复合。

Lambda 在函数式编程中的应用非常广泛,它可以用来实现高阶函数、函数组合、柯里化以及函数式编程风格的链式调用等。例如,我们可以使用 Lambda 实现一个使用柯里化技术的 add 函数:

// 带有柯里化技术的 Lambda 函数
const addCurry = a => b => a + b;

// 调用这个函数
console.log(addCurry(3)(4)); // 输出:7

在上述代码中,我们定义了一个带有柯里化技术的 Lambda 函数 addCurry(),它接受一个参数 a 并返回另一个 Lambda 函数,该函数接受参数 b 并返回两者之和。这样就可以实现类似于 add(3, 4) 的调用方式。

在 JavaScript 函数式编程中,Lambda 是一个非常重要的概念,它提供了一种更加灵活和简洁的函数定义方式,并且能够支持高阶函数、函数组合、柯里化以及函数式编程风格的链式调用等。如果能够熟练掌握 Lambda 表达式的使用,就可以大幅提升程序的可读性、可测试性及可维护性,使程序更加健壮和灵活。

2、Lambda演算 (Lambda Calculus)

Lambda演算,也称为Lambda计算,是一种用于描述函数的形式系统。它由数学家阿隆佐·邱奇在20世纪30年代提出,并被广泛运用于函数式编程语言(如Lisp、Scheme和Haskell)中。可以说Lambda演算是函数式编程思想的理论基础之一。

Lambda演算本身没有变量、数据类型或语句等概念,只有一个函数抽象的概念。一个函数抽象包括两部分:参数和函数体。例如,(x) -> x + 1 就是一个Lambda表达式,其中 x 是参数,x + 1 是函数体。

下面是一个简单的Lambda演算示例:

(λx.x + x)(2)

这个Lambda表达式表示一个函数,接收一个参数 x ,并返回 x + x 的值,然后对其输入参数 2 进行了求值。根据Lambda演算的规则,该表达式最终会被简化为 4

在JavaScript中,我们可以使用箭头函数来实现Lambda演算。下面是一个简单的示例代码:

// 使用 Lambda 表达式实现加法函数
const add = (a) => (b) => a + b;

// 使用 Lambda 表达式实现乘法函数
const mul = (a) => (b) => a * b;

// 调用这两个函数,并进行函数组合
const result = mul(3)(add(1)(2));

console.log(result); // 输出:9

在上述代码中,我们使用Lambda表达式实现了两个函数 addmul,它们分别表示加法和乘法的函数抽象。然后,我们使用函数组合的方式将它们组成一个表达式,并计算结果。

JavaScript中的Lambda演算还可以用于实现惰性求值、函数柯里化以及惰性加载等高级特性。例如,在下面的示例代码中,我们使用Lambda演算实现了一个惰性计算的斐波那契数列:

// 使用 Lambda 表达式实现惰性计算的斐波那契数列
const fibs = (a, b) => {
  return () => {
    const next = a + b;
    a = b;
    b = next;
    return next;
  };
};

// 打印前10个斐波那契数
const fib = fibs(0, 1);
for (let i = 0; i < 10; i++) {
  console.log(fib());
}

在上述代码中,我们使用Lambda表达式实现了一个惰性计算的斐波那契数列,即只有当需要时才会计算下一个数。这样可以节省计算资源,同时也使得程序更加灵活可控。

Lambda演算是函数式编程的基本理论之一,在JavaScript中可以使用箭头函数来实现Lambda表达式,并且可以应用于惰性求值、函数柯里化以及惰性加载等高级特性。如果能够熟练掌握Lambda演算的使用,就可以更加深入地理解函数式编程思想,并且在实际开发中提升程序的可读性和可维护性。

3、惰性求值 (Lazy evaluation)

惰性求值是指只有在需要时才计算表达式的值,而不是在定义时就立即计算。这种方式可以节省计算资源、提高程序的性能并且更加灵活地控制程序的执行流程。在函数式编程中,惰性求值特别重要,因为函数式编程通常使用无限数据结构或递归算法,这些运算需要大量的计算资源。

在JavaScript中,我们可以使用闭包和函数柯里化来实现惰性求值。下面是一个简单示例代码:

// 使用函数柯里化实现一个惰性计算的 add 函数
function add(a) {
  return function(b) {
    if (b) {
      return add(a + b);
    }
    return a;
  };
}

// 调用惰性计算的 add 函数,并输出结果
console.log(add(1)(2)(3)(4)()); // 输出:10

在上述代码中,我们使用函数柯里化实现了一个惰性求值的 add 函数。当调用 add(1)(2)(3)(4) 时,它返回一个新的函数,如果再次调用它并传入一个参数,则会继续创建新的函数,直到最后调用不带参数的函数,才进行求值并返回结果。这样可以实现惰性计算,避免了不必要的运算。

另外,在函数式编程中,我们也经常使用惰性求值来处理无限数据结构。例如,下面的示例代码演示了如何使用惰性计算的方式生成一个无限序列:

// 使用惰性求值实现一个无限序列
function* createSequence() {
  let i = 0;
  while (true) {
    yield i;
    i++;
  }
}

// 打印前10个数字
const seq = createSequence();
for (let i = 0; i < 10; i++) {
  console.log(seq.next().value);
}

在上述代码中,我们使用ES6的生成器函数 createSequence 来创建一个无限序列,每次调用 next() 方法都会返回下一个数字。由于生成器函数是基于惰性求值的机制实现的,因此可以实现无限数据结构的操作。

惰性求值是函数式编程中的一个重要概念,它可以帮助我们更有效地处理大量的运算和无限的数据结构。在JavaScript中,我们可以使用闭包和函数柯里化来实现惰性求值,并且在函数式编程中应用广泛。

4、幺半群 (Monoid)

在函数式编程中,幺半群(Monoid)是指一个满足以下条件的数学结构:

  1. 存在一个元素(称为幺元),使任何元素与之结合得到自身
  2. 该结构满足结合律:任意三个元素 a、b、c,满足 (a+b)+c=a+(b+c)(a + b) + c = a + (b + c)

在JavaScript中,我们可以使用对象字面量来定义一个幺半群,并实现一些常见的方法。下面是一个简单的示例代码:

// 定义一个数字加法幺半群
const sumMonoid = {
  // 定义幺元
  empty: 0,
  // 实现结合方法
  concat: (a, b) => a + b
}

// 定义一个字符串拼接幺半群
const stringMonoid = {
  // 定义幺元
  empty: '',
  // 实现结合方法
  concat: (a, b) => a.concat(b)
}

在上述代码中,我们分别定义了一个数字加法幺半群和一个字符串拼接幺半群,并实现了它们的 emptyconcat 方法。这两个方法分别对应了幺半群的幺元和结合律。

除此之外,在函数式编程中,我们也经常使用幺半群来实现一些高阶函数。例如,下面的代码演示了如何使用幺半群结合 reduce 方法来实现一个通用的 sum 函数:

// 使用幺半群结合 reduce 方法实现一个通用的 sum 函数
function sum(arr) {
  return arr.reduce(sumMonoid.concat, sumMonoid.empty);
}

// 调用 sum 函数,并输出结果
console.log(sum([1, 2, 3, 4, 5])); // 输出:15

在上述代码中,我们定义了一个 sum 函数,它接收一个数组作为参数,并使用幺半群的 concatempty 方法来计算数组元素的和。

另外,幺半群还可以用于函数组合。例如,下面的示例代码演示了如何使用幺半群实现一个 compose 函数:

// 定义一个字符串反转幺半群
const reverseMonoid = {
  empty: '',
  concat: (a, b) => b + a
}

// 使用幺半群实现一个函数组合
function compose(f, g) {
  return function(x) {
    return f(g(x));
  };
}

// 定义一个反转字符串的函数
const reverse = s => s.split('').reduce(reverseMonoid.concat, reverseMonoid.empty);

// 定义一个求平方的函数
const square = x => x * x;

// 组合两个函数
const reverseSquare = compose(square, reverse);

// 调用 reverseSquare 函数,并输出结果
console.log(reverseSquare('hello')); // 输出:olleh

在上述代码中,我们定义了一个字符串反转幺半群,并使用它作为组合函数的运算方式,实现了一个 compose 函数来组合两个函数。其中,reverse 函数用于反转字符串,square 函数用于求平方,最后通过 compose 函数将这两个函数组合起来。

5、单子 (Monad)

在函数式编程中,单子(Monad)是一种范畴论的概念,在JavaScript中可以被视为一种具有特定结构的对象。它包含了一个值和一系列操作这个值的方法,并且这些方法可以顺序组合起来。

在JavaScript中,单子的标准实现方式是通过一个类来实现,这个类需要满足以下条件:

  1. 包含一个值
  2. 声明一个 of 静态方法,用于创建一个新的单子
  3. 实现一个 map 方法,用于对单子中的值进行变换
  4. 实现一个 chain 方法,用于连接其他单子或普通函数

下面是一个简单的示例代码,展示了如何使用单子实现数据的链式处理:

// 定义一个 Maybe 单子
class Maybe {
  constructor(value) {
    this._value = value;
  }

  // 实现 of 静态方法,用于创建一个新的 Maybe 单子
  static of(value) {
    return new Maybe(value);
  }

  // 实现 map 方法,用于对单子中的值进行变换
  map(fn) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this._value));
  }

  // 实现 chain 方法,用于连接其他单子或普通函数
  chain(fn) {
    return this.map(fn).join();
  }

  // 定义一个 join 方法,用于连接两个 Maybe 单子
  join() {
    return this.isNothing() ? Maybe.of(null) : this._value;
  }

  // 定义一个 isNothing 方法,用于判断单子是否为空
  isNothing() {
    return this._value === null || this._value === undefined;
  }
}

// 使用 Maybe 单子进行数据处理
const getUser = id => Maybe.of(id)
  .map(findUserById)   // 查找用户
  .chain(getUserPosts); // 获取用户的帖子

// 查找用户
function findUserById(id) {
  const user = {id: 1, name: 'John', age: 28};
  return Maybe.of(user[id === user.id ? 'name' : 'age']);
}

// 获取用户的帖子
function getUserPosts(name) {
  const posts = [
    {title: 'Post 1', author: 'John'},
    {title: 'Post 2', author: 'Mary'},
    {title: 'Post 3', author: 'John'}
  ];
  return Maybe.of(posts.filter(post => post.author === name));
}

// 调用 getUser 函数,并输出结果
getUser(1).map(console.log); // 输出:[{title: 'Post 1', author: 'John'}, {title: 'Post 3', author: 'John'}]

在上述代码中,我们定义了一个 Maybe 单子,它包含了一个值和一系列操作这个值的方法。其中,of 方法用于创建一个新的 Maybe 单子,map 方法用于对单子中的值进行变换,join 方法用于连接两个 Maybe 单子,而 chain 方法则用于连接其他单子或普通函数。

最后,我们使用 Maybe 单子来进行数据处理。具体地,我们先通过 findUserById 函数查找用户,并将结果映射为用户的名字或年龄;然后,再通过 getUserPosts 函数获取用户的帖子,并将结果返回。整个过程中,我们都使用了 Maybe 单子来处理不确定性的情况,确保在出现错误时程序不会崩溃。

余单子 (Comonad)

在函数式编程中,余单子(Comonad)是一种范畴论的概念,它是单子的对偶。与单子用于将值与方法组合起来并进行处理不同,余单子用于从值中提取出其他值来进行处理。

在JavaScript中,余单子的标准实现方式是通过一个类来实现,这个类需要满足以下条件:

  1. 包含一个值
  2. 实现一个 extract 方法,用于提取出值中的元素
  3. 实现一个 map 方法,用于对余单子中的值进行变换
  4. 实现一个 extend 方法,用于将余单子提取出的多个值扩展为新的余单子

下面是一个简单的示例代码,展示了如何使用余单子实现数据的处理:

// 定义一个 List 余单子
class List {
  constructor(values) {
    this._values = values;
  }

  // 实现 extract 方法,用于提取出第一个元素
  extract() {
    return this._values[0];
  }

  // 实现 map 方法,用于对余单子中的值进行变换
  map(fn) {
    return new List(this._values.map(fn));
  }

  // 实现 extend 方法,用于将余单子提取出的多个值扩展为新的余单子
  extend(fn) {
    return new List(this._values.map((_, i) => fn(new List(this._values.slice(i)))));
  }
}

// 使用 List 余单子进行数据处理
const list = new List([1, 2, 3]);

// 提取出第一个元素
console.log(list.extract()); // 输出:1

// 对余单子中的值进行变换
console.log(
  list
    .map(n => n * 2)
    .extract()
); // 输出:2

// 将余单子提取出的多个值扩展为新的余单子
console.log(
  list
    .extend(x => x.extract().reduce((a, b) => a + b, 0))
    .extract()
); // 输出:6

在上述代码中,我们定义了一个 List 余单子,它包含了一组值和一系列操作这些值的方法。其中,extract 方法用于提取出第一个元素,map 方法用于对余单子中的值进行变换,extend 方法则用于将余单子提取出的多个值扩展为新的余单子。

最后,我们使用 List 余单子来进行数据处理。具体地,我们先通过 extract 方法提取出第一个元素,然后通过 map 方法将余单子中的每个元素都乘以2,并提取出结果;最后,使用 extend 方法将余单子提取出的多个值求和,并返回结果。整个过程中,我们都使用了 List 余单子来提取和处理多个值,从而实现了数据的高效处理。

6、应用函子 (Applicative Functor)

在函数式编程中,函子(Functor)是一种将值与方法组合起来进行处理的概念。其中,函子必须满足以下条件:首先它必须包含一个值,其次它必须实现 map 方法,用于对函子中的值进行变换。

然而,在某些场景下,我们需要对多个函子中的值进行变换,并将结果再组合起来返回。这时,我们就需要使用函子的扩展形式——应用函子(Applicative Functor)。

应用函子除了具有函子的特性外,还需要实现一个 ap 方法,用于在不同的应用函子之间进行组合操作。具体地,ap 方法接受另一个应用函子作为参数,将两个应用函子中的值进行组合,并返回新的应用函子。

下面是一个简单的示例代码,展示了如何使用应用函子实现数据的处理:

// 定义一个 Maybe 应用函子
class Maybe {
  constructor(value) {
    this._value = value;
  }

  // 实现 map 方法,用于对函子中的值进行变换
  map(fn) {
    return this._value === null || this._value === undefined ? new Maybe(null) : new Maybe(fn(this._value));
  }

  // 实现 ap 方法,用于在不同的应用函子之间进行组合操作
  ap(functor) {
    return this._value === null || this._value === undefined ? new Maybe(null) : functor.map(this._value);
  }
}

// 使用 Maybe 应用函子进行数据处理
const add = x => y => x + y;

const maybeA = new Maybe(2);
const maybeB = new Maybe(3);

// 对多个应用函子中的值进行变换,并将结果再组合起来返回
console.log(
  maybeA
    .map(add)
    .ap(maybeB)
); // 输出:5

在上述代码中,我们定义了一个 Maybe 应用函子,它包含一个值和一系列操作这个值的方法。其中,map 方法用于对函子中的值进行变换,ap 方法则用于在不同的应用函子之间进行组合操作。

最后,我们使用 Maybe 应用函子来进行数据处理。具体地,我们先将柯里化函数 add 封装为函数式的形式(即返回一个返回值的函数),然后使用两个 Maybe 应用函子分别传入 add 函数中,并通过 ap 方法进行组合。最终,我们得到了一个新的 Maybe 应用函子,其中包含了两个应用函子中的值相加的结果。整个过程中,我们都使用了 Maybe 应用函子来组合和处理多个值,从而实现了数据的高效处理。

7、态射 (Morphism)

在函数式编程中,态射(Morphism)是一个将类型中的值进行转化的映射关系。主要分为三种类型:函数(Function)、自然变换(Natural Transformation)和范畴化 (Categorification)。

其中,函数是最常见的一种态射,它将一个输入值映射为一个输出值。具体地,在 JavaScript 中就是普通的函数声明或匿名函数表达式:

// 声明一个函数,接受一个参数并返回一个值
function add(num) {
  return num + 1;
}

// 匿名函数表达式,实现乘法操作
const multiply = (num1, num2) => num1 * num2;

除了普通函数外,我们还可以使用柯里化函数来进行函数的组合和变换。具体地,柯里化函数将多个参数的函数转化为接受单个参数的函数序列,从而简化了函数的处理流程,并方便了代码的组合和复用:

// 将普通函数转化为柯里化函数
const curriedAdd = num1 => num2 => num1 + num2;

// 使用柯里化函数进行函数组合和变换
const composedFn = curriedAdd(1)
  .compose(curriedAdd(2))
  .compose(curriedAdd(3));
console.log(composedFn(4)); // 输出:10

除了函数以外,自然变换(Natural Transformation)是另一种常见的态射。它将一个范畴中的对象转化为另一个范畴中的对象,同时保持范畴结构的不变。具体地,在 JavaScript 中,我们可以使用类或对象来实现自然变换。例如:

// 定义一个自然变换
class MaybeToEither {
  // 接受一个 maybe 对象,返回对应的 either 对象
  static transform(maybe) {
    return maybe.isNull() ? Either.left("value is null") : Either.right(maybe.get());
  }
}

// 使用自然变换将 Maybe 对象转化为 Either 对象
const maybeValue = new Maybe(42);
const eitherValue = MaybeToEither.transform(maybeValue);
console.log(eitherValue); // 输出:Right { _value: 42 }

在上述代码中,我们定义了一个 MaybeToEither 类,实现了从 Maybe 对象到 Either 对象的自然变换。具体地,我们通过 transform 方法接受一个 Maybe 对象,并根据其是否为 null 来返回一个对应的 Either 对象。整个过程中,我们使用了自然变换这一范畴理论中的概念,将范畴之间的映射关系进行了转化。

总之,态射(Morphism)是函数式编程中的重要概念,它能够帮助我们对不同类型和结构中的值进行转化和处理,从而实现更加高效和灵活的程序设计。

7.1 自同态(Endomorphism)

在函数式编程中,自同态(Endomorphism)是一种输入和输出类型相同的函数。具体地,它将一个类型中的值进行转化,得到一个新的值,并且该值的类型与原始类型相同。

在 JavaScript 中,自同态可以用普通函数或函数组合来实现。例如:

// 普通函数实现自同态
function double(num) {
  return num * 2;
}

// 函数组合实现自同态
const addOne = num => num + 1;
const square = num => num * num;

const composedFn = addOne
  .compose(square)
  .compose(addOne)
  .compose(double);

console.log(composedFn(3)); // 输出:41

在上述代码中,我们定义了三个自同态函数 doubleaddOnesquare。其中,double 接受一个数值并将其乘以 2,addOne 实现加一操作,square 实现平方操作。然后,我们使用函数组合将这些函数组合在一起,生成一个新的自同态函数 composedFn。最后,我们调用 composedFn 函数,传入一个初始值,并得到了一个新的结果。

除了普通函数和函数组合以外,自同态还可以通过类或对象来实现。例如:

// 定义一个自同态类
class Increment {
  static apply(value) {
    return value + 1;
  }
}

// 使用自同态类对数组中的每个元素进行加一操作
const numbers = [1, 2, 3, 4, 5];
const incrementedNumbers = numbers.map(Increment.apply);
console.log(incrementedNumbers); // 输出:[ 2, 3, 4, 5, 6 ]

在上述代码中,我们定义了一个 Increment 类,实现了加一操作的自同态。具体地,我们通过静态方法 apply 接受一个数值,并将其加一后返回。然后,我们使用该类对数组中的每个元素进行转化,得到一个新的包含加一后数值的数组。

自同态(Endomorphism)是函数式编程中的一个重要概念,它能够帮助我们对同一类型的值进行转化和处理,从而简化了程序设计和实现过程。不同于其它范畴理论中的概念,自同态仅对输入和输出类型相同的映射关系进行了描述,使得相关的操作更加直观和易于理解。

7.2 同构(Isomorphism)

在函数式编程中,同构(Isomorphism)是指两个数据类型之间的一种映射关系,它们可以互相转化并且保持其结构不变。

在 JavaScript 中,我们可以通过定义两个自同态(Endomorphism)来实现同构。具体地,我们可以定义一个从 A 类型到 B 类型的自同态和一个从 B 类型到 A 类型的自同态,这样就能够将 A 类型的值转化为 B 类型,同时也能将 B 类型的值转化为 A 类型。

例如,假设我们有一个包含名称和年龄信息的对象 Person:

const Person = {
  name: 'John',
  age: 30,
};

我们可以定义一个将 Person 对象转化为数组 [name, age] 的自同态:

const personToArray = person => [person.name, person.age];

同时,我们也可以定义一个将数组 [name, age] 转化为 Person 对象的自同态:

const arrayToPerson = ([name, age]) => ({ name, age });

注意到这两个自同态函数满足以下条件:

// 原始值转化为目标值再转化回原始值
arrayToPerson(personToArray(Person)) // 输出:{ name: 'John', age: 30 }

// 目标值转化为原始值再转化回目标值
personToArray(arrayToPerson([Person.name, Person.age])) // 输出:[ 'John', 30 ]

// 自同态函数可以连续组合
const composedFn = personToArray.compose(arrayToPerson);
composedFn(Person) // 输出:{ name: 'John', age: 30 }

由于这两个自同态函数之间存在双向的映射关系,所以它们实现了同构的转化。这种技术在编写程序时非常有用,可以帮助我们将不同的数据类型互相转换,并且保持其结构不变。

举个例子,假设我们需要将一个对象转化为 JSON 格式并进行网络传输。由于 JSON 只支持标准的数据类型(如字符串、数字、布尔值等),因此需要将对象转化为一些标准类型的集合。正如我们所看到的那样,通过定义一个自同态函数和它的逆函数,我们可以很容易地将 JavaScript 对象转化为 JSON 字符串并反之。这种方式使得我们能够方便地进行数据序列化和反序列化,从而避免了手动处理复杂数据类型的繁琐过程。

7.3 同态(Homomorphism)

在函数式编程中,同态(Homomorphism)是指两个代数结构之间的一种映射关系,它能够保持这两个代数结构之间的运算行为不变。在 JavaScript 中,我们可以使用同态函数来实现这种映射关系。

具体来说,如果有两个代数结构 A 和 B,一个从 A 到 B 的同态函数 h 能够保证对于 A 上的每个运算 op,在 B 中对应的运算也是相同的。即:

h(op(a, b)) = op(h(a), h(b))

其中 a 和 b 是 A 上的元素,op 是 A 上的运算符。

举一个简单的例子,假设我们有两个数字类型 A 和 B,A 是普通的整数,B 是另一种类型的数字,它们的值由字符串表示。我们可以定义一个从 A 到 B 的同态函数,将整数转换为字符串类型的数字:

const intToString = (int) => String(int);

使用这个同态函数,我们可以保证在处理 A 类型的运算时,只需要将其转换为 B 类型的值、进行 B 类型的运算,最后再将结果转换回 A 类型即可。例如,下面是一个求和的函数:

const add = (a, b) => a + b;

如果要将其用于操作 A 类型的数字,只需要先将两个数字转化为 B 类型的字符串,然后进行 B 类型的运算,最后将结果转化为 A 类型即可:

const sum = (a, b) => intToString(add(intToString(a), intToString(b)));

由于 intToString 这个同态函数保证了整数类型和字符串类型之间的运算行为不变,因此我们可以在操作字符串类型的数字时,安全地使用 add 函数。这种方式使得我们能够方便地处理不同类型之间的运算,从而降低了代码的复杂度。

另一个有用的例子是将一个复杂的数据类型映射到另一个数据类型上。例如,假设我们有一个包含用户信息的对象:

const user = {
  name: 'Alice',
  age: 30,
  address: {
    city: 'New York',
    state: 'NY',
  },
};

我们可以定义一个从用户对象到地址字符串的同态函数:

const userToAddressString = ({ address }) =>
  `${address.city}, ${address.state}`;

这个同态函数可以被用于将用户对象转换为地址字符串。同时,我们也可以在地址字符串上定义一些额外的操作,例如计算距离或查找其他具有相似地址的用户。这种方式使得我们能够方便地处理复杂的数据结构,并且将其映射到另一个更简单的数据类型上进行处理。

7.4 变形(Catamorphism)

在函数式编程中,Catamorphism(或称为 fold)是一种通用的归纳原语,它可以被用于将一个数据结构展开成另一个值。更具体来说,Catamorphism 可以将一个代数数据类型的定义转换为一个普通的 JavaScript 函数。

在 JavaScript 中,我们通常使用 reduce 函数来实现 Catamorphism。reduce 函数接收两个参数:一个归约函数和一个初始值。归约函数接收两个参数,第一个参数是累加器(accumulator),第二个参数是当前遍历到的元素。归约函数的返回值会作为下一次调用时的累加器传入。最终,reduce 函数会返回归约函数的最后一次调用的返回值作为结果。

举一个简单的例子,假设我们有一个数字数组,我们想要对其进行求和。我们可以使用 reduce 函数来实现:

const sum = (arr) => arr.reduce((acc, x) => acc + x, 0);

在这个例子中,归约函数 (acc, x) => acc + x 接收两个参数:累加器 acc 和当前遍历到的元素 x,并将它们相加作为下一次调用时的累加器。初始化时,累加器的值为 0。

Catamorphism 不仅能够用于数组,还可以被用于其他各种数据结构,例如树、列表、图等。举一个简单的例子,假设我们有一个树形结构:

const tree = {
  value: 1,
  children: [
    { value: 2, children: [] },
    { value: 3, children: [{ value: 4, children: [] }] },
  ],
};

我们可以使用 Catamorphism 来计算所有节点值的和。首先,我们需要将树展开成一个数组。这可以通过递归地访问树来实现:

const flattenTree = ({ value, children }) => [
  value,
  ...children.flatMap(flattenTree),
];

这个函数接收一个节点,并返回一个由节点值和其子节点值展开的数组。flatMap 函数用于将多个子数组合并为一个数组。

然后我们可以使用 reduce 函数对数组进行求和:

const sumTree = (tree) =>
  flattenTree(tree).reduce((acc, x) => acc + x, 0);

这个函数先将树展开为数组,然后使用 reduce 函数对数组进行求和。

Catamorphism 的一个重要应用是定义递归函数。例如,如果我们想要定义一个从树到数字的同态函数,计算树的深度,我们可以这样实现:

const depth = ({ children }) =>
  children.reduce((acc, child) => Math.max(acc, depth(child)), 0) + 1;

这个函数接收一个节点并返回该节点的深度(也就是到叶子节点的最长路径长度)。它使用 reduce 函数递归地遍历所有子节点,并返回最大深度加 1。这个函数使用了 Catamorphism 来对树进行递归计算,从而实现了一种简单而通用的递归方式。

7.5 失真(Anamorphism)

在函数式编程中,Anamorphism 是一个通用的分解原语,它可以将一个值拆分成一系列子值。更具体来说,Anamorphism 可以将一个迭代生成器(或类似的生成函数)转换为一个普通的 JavaScript 函数。

在 JavaScript 中,我们通常使用 unfold 函数来实现 Anamorphism。unfold 函数接收两个参数:一个生成函数和一个初始值。生成函数接收一个值,并返回一个包含两个元素的数组:下一个值和新状态。如果生成函数返回 undefined,则 unfold 函数停止迭代。

举一个简单的例子,假设我们想要生成一个斐波那契数列。我们可以使用 unfold 函数来实现:

const fibonacci = (n) =>
  Array.from({ length: n }, (_, i) => i).reduce(
    ([x, y]) => [y, x + y],
    [0, 1]
  )[0];

const unfold = (f, init) => {
  const result = [];
  let state = init;
  while (true) {
    const next = f(state);
    if (next === undefined) {
      break;
    } else {
      const [value, newState] = next;
      result.push(value);
      state = newState;
    }
  }
  return result;
};

const fibs = (n) =>
  unfold(([x, y]) => [x, [y, x + y]], [0, 1]).slice(0, n);

console.log(fibs(10)); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

在这个例子中,我们首先定义了一个生成数列的函数 fibonacci。然后我们实现了 unfold 函数,并传入一个生成函数和一个初始值。生成函数接收一个状态(由上一次调用返回的值)并返回一个新的值和新的状态。在这个例子中,我们将斐波那契数列的生成方式 (x,y) => (x,[y,x+y]) 传递给了 unfold 函数。

Anamorphism 还可以被用于构造各种数据结构,例如树、列表、图等。举一个简单的例子,假设我们想要生成一个从 1 开始的无限自然数序列。我们可以使用 Anamorphism 来实现:

const unfold = (f, init) => ({
  [Symbol.iterator]: function* () {
    let state = init;
    while (true) {
      const next = f(state);
      if (next === undefined) {
        break;
      } else {
        const [value, newState] = next;
        yield value;
        state = newState;
      }
    }
  },
});

const naturals = unfold((n) => [n, n + 1], 1);

console.log([...naturals].slice(0, 10)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

在这个例子中,我们定义了一个 unfold 函数,它接收一个生成函数和一个初始状态并返回一个迭代器对象。生成函数接收一个状态并返回下一个值和新状态。在这个例子中,我们将 (n) => [n, n + 1] 作为生成函数,它接收一个数字并返回该数字和下一个数字。我们使用 unfold 函数来实现了一个无限自然数序列。

Anamorphism 的另一个重要应用是定义无限递归数据结构。例如,如果我们想要定义一个无限的二叉树,我们可以这样实现:

const { map } = require("ramda");

const iteratee = (f) => (x) => [x, f(x)];

const tree = (f) => (seed) => ({
  value: seed,
  left: () => tree(f)(f(seed)[0]),
  right: () => tree(f)(f(seed)[1]),
});

const depthFirstTraversal = function* (root) {
  if (root !== undefined && root !== null) {
    yield root.value;
    yield* depthFirstTraversal(root.left());
    yield* depthFirstTraversal(root.right());
  }
};

const take = (n) => (iter) => Array.from({ length: n }, (_, i) => iter[i]);

const drawTree = (node) => {
  const lines = [];
  const traverse = (node, prefix, isTail) => {
    lines.push(`${prefix}${isTail ? "└── " : "├── "}${node.value}`);
    if (node.left() !== undefined) {
      traverse(node.left(), `${prefix}${isTail ? "    " : "│   "}`, false);
    }
    if (node.right() !== undefined) {
      traverse(node.right(), `${prefix}${isTail ? "    " : "│   "}`, true);
    }
  };
  traverse(node, "", true);
  return lines.join("\n");
};

const infiniteTree = tree(iteratee((x) => [2 * x, 2 * x + 1]))(1);
console.log(drawTree(infiniteTree));

这个例子中,我们定义了 iteratee 函数用于转换函数,将一个节点的值转换为其左右子节点的值。然后我们定义了 tree 函数,该函数接收一个 iteratee 函数作为参数,并返回一个树形结构,其中每个节点都有一个左右子节点。

我们使用 depthFirstTraversal 函数遍历该树,并使用 take 函数从遍历结果中取出前 15 个元素。使用 drawTree 函数可以将树以 ASCII 图形方式打印出来。这个无限树不会自动扩展到所有深度,但可以无限地向下延伸。Anamorphism 可以用于实现任何无限递归数据结构。

7.6 Hylomorphism

在函数式编程中,Hylomorphism 是一个通用的组合原语,它由 Anamorphism 和 Catamorphism 两个部分组成。Anamorphism 将一个一般的迭代器转换为一个纯函数,而 Catamorphism 则将一个数据结构折叠成单个值。Hylomorphism 的含义是从初始值开始生成一个数据结构,然后将该数据结构折叠成一个单独的值。

在 JavaScript 中,我们通常使用 hylo 函数来实现 Hylomorphism。hylo 函数接收三个参数:一个生成函数、一个归纳函数和一个初始值。生成函数接收一个状态并返回包含下一个值和新状态的数组。如果下一个值为 undefined,则 hylo 函数停止迭代。归纳函数接收当前值和下一个值,并返回一个新值。

举一个简单的例子,假设我们想要计算斐波那契数列的总和。我们可以使用 hylo 函数来实现:

const fibonacci = (n) =>
  Array.from({ length: n }, (_, i) => i).reduce(
    ([x, y]) => [y, x + y],
    [0, 1]
  )[0];

const unfold = (f, init) => {
  const result = [];
  let state = init;
  while (true) {
    const next = f(state);
    if (next === undefined) {
      break;
    } else {
      const [value, newState] = next;
      result.push(value);
      state = newState;
    }
  }
  return result;
};

const hylo = (f, g, init) => {
  const recur = (state) => {
    const next = f(state);
    if (next === undefined) {
      return init;
    } else {
      const [value, newState] = next;
      return g(value, recur(newState));
    }
  };
  return recur(init);
};

const fibs = (n) =>
  unfold(([x, y]) => [x, [y, x + y]], [0, 1]).slice(0, n);

const sumFibs = (n) =>
  hylo(
    ([x, y]) => [x, [y, x + y]],
    ([x, y], sum) => (x <= n ? sum + x : sum),
    [0, 1]
  );

console.log(sumFibs(10)); // 44

在这个例子中,我们首先定义了 fibonacci 函数和 unfold 函数,它们与前面斐波那契数列的例子相同。然后我们实现了 hylo 函数,并传入一个生成函数、一个归纳函数和一个初始值。生成函数接收一个状态并返回下一个值和新状态。在这个例子中,我们将斐波那契数列的生成方式 (x,y) => (x,[y,x+y]) 传递给了 hylo 函数。

我们使用归纳函数 ([x, y], sum) => (x <= n ? sum + x : sum) 将当前斐波那契数列元素的值加入总和中,如果超过了 n 则返回之前的总和。在这个例子中,我们将 [0, 1] 作为初始状态传递给 hylo 函数。

Hylomorphism 还可以被用于构造各种数据结构,例如树、列表、图等。举一个简单的例子,假设我们要计算一个二叉树的所有节点值之和。我们可以使用 Hylomorphism 来实现:

const { map } = require("ramda");

const iteratee = (f) => (x) => [x, f(x)];

const tree = (f) => (seed) => ({
  value: seed,
  left: () => tree(f)(f(seed)[0]),
  right: () => tree(f)(f(seed)[1]),
});

const depthFirstTraversal = function* (root) {
  if (root !== undefined && root !== null) {
    yield root.value;
    yield* depthFirstTraversal(root.left());
    yield* depthFirstTraversal(root.right());
  }
};

const take = (n) => (iter) => Array.from({ length: n }, (_, i) => iter[i]);

const drawTree = (node) => {
  const lines = [];
  const traverse = (node, prefix, isTail) => {
    lines.push(`${prefix}${isTail ? "└── " : "├── "}${node.value}`);
    if (node.left() !== undefined) {
      traverse(node.left(), `${prefix}${isTail ? "    " : "│   "}`, false);
    }
    if (node.right() !== undefined) {
      traverse(node.right(), `${prefix}${isTail ? "    " : "│   "}`, true);
    }
  };
  traverse(node, "", true);
  return lines.join("\n");
};

const sumValues = (tree) =>
  hylo(
    ({ value, left, right }) => [value, [left, right]],
    ([value, [leftSum, rightSum]]) => value + leftSum + rightSum,
    tree
  );

const t = tree(iteratee((x) => [2 * x, 2 * x + 1]))(1);
console.log(drawTree(t));
console.log(sumValues(t)); // 255

在这个例子中,我们首先定义了 iteratee 函数用于转换函数,将一个节点的值转换为其左右子节点的值。然后我们定义了 tree 函数,该函数接收一个 iteratee 函数作为参数,并返回一个树形结构,其中每个节点都有一个左右子节点。

我们使用 depthFirstTraversal 函数遍历该树,并使用 take 函数从遍历结果中取出前 15 个元素。使用 drawTree 函数可以将树以 ASCII 图形方式打印出来。 使用归纳函数 ([value, [leftSum, rightSum]]) => value + leftSum + rightSum 计算所有节点的值之和.

在这个例子中,我们将树的根作为初始状态传递给 hylo 函数,将生成函数 { value: seed, left: () => left(seed), right: () => right(seed) } 传递给 hylo 函数。在这个例子中,hylo 函数将递归地折叠整个二叉树,并计算所有节点的值之和。

7.7 Paramorphism

在函数式编程中,Paramorphism 是另一种通用的组合原语,它由 Histomorphism 和 Futumorphism 两个部分组成。Histomorphism 将一个数据结构折叠成一个“历史”(即折叠过程的所有状态),而 Futumorphism 则使用该“历史”来生成新的数据结构。Paramorphism 的含义是先折叠数据结构并记录下其中间结果,再根据这些中间结果构建新的数据结构。

在 JavaScript 中,我们通常使用 para 函数来实现 Paramorphism。para 函数接收三个参数:一个归纳函数、一个生成函数和一个数据结构。归纳函数接收当前值、累积器和“历史”,并返回一个新的累积器。在折叠过程中,Para 会将每次迭代的结果添加到“历史”中。最后,Para 将“历史”作为初始值传递给 Futu 函数,从而生成新的数据结构。

下面是一个简单的例子,假设我们有一个包含数字的数组,我们想要计算某个数字左侧数字之和与右侧数字之和的平均值。我们可以使用 Para 来实现:

const { unfold } = require("ramda");

const arrayToTree = (arr) => {
  const f = (i) => [
    arr[i],
    i * 2 + 1 < arr.length ? f(i * 2 + 1) : undefined,
    i * 2 + 2 < arr.length ? f(i * 2 + 2) : undefined,
  ];
  return f(0);
};

const treeToArray = (tree) =>
  unfold((node) => {
    if (node === undefined) {
      return undefined;
    } else {
      const [value, left, right] = node;
      return [
        value,
        left !== undefined ? left : undefined,
        right !== undefined ? right : undefined,
      ];
    }
  }, tree);

const para = (alg, tree) => {
  const recur = (t, acc) =>
    alg(
      t[0],
      acc,
      t[1].map((x) => recur(x, { ...acc, history: [...acc.history, t[0]] }))
    );
  return recur(tree, { history: [] }).result;
};

const futu = (coalg, seed) => {
  const recur = (s) => {
    const [value, children] = coalg(s);
    if (children === undefined) {
      return value;
    } else {
      return [value, children.map(recur)];
    }
  };
  return recur(seed);
};

const averageOfSums = (arr) => {
  const sum = para(
    (value, { leftSum, rightSum, count, history }) => ({
      leftSum: leftSum + history.reduce((a, b) => a + b, 0),
      rightSum: rightSum + value * count,
      count: count + 1,
    }),
    arrayToTree(arr)
  );
  return sum.rightSum / sum.count - sum.leftSum / sum.count;
};

console.log(averageOfSums([1, 2, 3, 4, 5])); // -0.6

在这个例子中,我们首先定义了 arrayToTree 和 treeToArray 函数,用于将数组转换为树形结构以及将树形结构转换回数组。然后我们定义了 para 和 futu 函数,para 函数使用归纳函数计算每个节点之前的所有数据之和、之后所有数据之和和节点数等信息,并将这些信息记录到历史中。最后,使用 Futu 来从历史中生成新数组。

在这个例子中,我们将 arrayToTree 的结果作为初始状态传递给 para 函数,并将一个对象 literal 作为累加器传递给 para 函数。累加器包含左侧数字之和、右侧数字之和、节点计数和历史数组等属性。归纳函数 ({ leftSum, rightSum, count, history }) => ({leftSum: leftSum + history.reduce((a, b) => a + b, 0),rightSum: rightSum + value * count,count: count + 1,}) 计算该节点之前的所有数据之和、之后所有数据之和和节点数等信息。最后,我们返回累积器中的 rightSum 与 leftSum 的差值除以节点数的结果。

总之,Paramorphism 是一种非常强大的组合原语,在函数式编程中具有广泛的应用。可以使用 Paramorphism 来计算任何类型的可折叠数据结构的“历史”,并基于该历史构建新的数据结构。这使得 Paramorphism 成为解决许多复杂问题的有力工具。

7.8 拟态性(Apomorphism)

在函数式编程中,Apomorphism 是另一种通用的组合原语,它由 Anamorphism 和 Futumorphism 两个部分组成。Anamorphism 将一个初始值转换为一个数据结构,而 Futumorphism 则使用该数据结构生成新的数据结构。Apomorphism 的含义是先根据初始值生成一个数据结构,再根据该数据结构构建新的数据结构。

在 JavaScript 中,我们通常使用 apo 函数来实现 Apomorphism。apo 函数接收两个参数:一个生成函数和一个初始值。生成函数以初始值作为输入,并返回一个对象或数组,表示生成的数据结构的当前节点以及下一步迭代时应该使用的新初始值。Futu 函数将可计算此数据结构的归纳函数用于结果。

下面是一个简单的例子,假设我们要生成一个包含斐波那契数列前 N 个数字的列表。我们可以使用 Apo 来实现:

const apo = (coalg, seed) => {
  const recur = (s) => {
    const { value, nextState } = coalg(s);
    return value ? [value, ...recur(nextState)] : [];
  };
  return recur(seed);
};

const fibonacci = (n) =>
  apo(
    (i) => ({
      value: i === 0 || i === 1 ? i : undefined,
      nextState: i === 0 ? null : i - 1,
    }),
    n
  );

console.log(fibonacci(10)); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

在这个例子中,我们首先定义了 apo 函数,使用给定的初始值递归地调用生成函数,并返回生成的列表。生成函数的结构非常简单,在每次迭代后我们检查当前数字是否小于等于 1,如果是则返回该数字;否则,我们返回 undefined。然后,我们将下一个状态设置为 i-1 或者 null,以便在生成过程结束时停止递归。

最后,我们通过调用 fibonacci 函数来生成包含前 N 个斐波那契数列的列表。传递给 apo 函数的初始值为 n,生成函数会从 n 开始并向下计算,直到达到基线情况(i=0 或 i=1)或到达指定的深度。在这个例子中,apoFutu 的迭代方式使得我们可以轻松地生成任何可逆数据结构,而无需编写许多重复代码。

Apomorphism 是一种非常强大的组合原语,在函数式编程中具有广泛的应用。可以使用 Apomorphism 来从初始值生成可逆数据结构,并基于该数据结构构建新的数据结构。这使得 Apomorphism 成为解决许多复杂问题的有力工具。

8、Setoid

在函数式编程中,Setoid 是一种类型类(Type Class)的概念,用于比较两个对象是否相等。一个 Setoid 实现必须具有 equals 方法,该方法接受另一个对象作为参数,并返回 true 或 false,以指示两个对象是否相等。通常,一个 Setoid 和一个 equals 方法是通过原型继承添加到每个需要进行相等性比较的对象中的。

下面是一个简单的例子,其中我们定义了一个名为 Point 的类,并实现了 equals 方法来检查两个点是否相等:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  equals(other) {
    return other instanceof Point && this.x === other.x && this.y === other.y;
  }
}

在这个例子中,我们首先定义了一个名为 Point 的类,它包含 x 和 y 坐标。然后,我们实现了 equals 方法,该方法检查另一个对象是否是 Point 类型的实例,并且其 x 和 y 坐标与当前点的坐标相同。注意,equals 方法不仅要检查传入的对象类型和属性值,而且还要使用 instanceof 运算符来确保给定的对象是 Point 类型的实例。

Setoid 具有很多应用场景,例如在测试代码中,可以使用它来比较预期结果和实际输出是否相等。另外,在使用 JavaScript 中的许多集合(如数组、集合和字典)时,Setoid 可以用来比较集合中的元素是否相等。下面是一个使用 Setoid 比较两个数组是否具有相同元素的例子:

const Setoid = {
  equals: (a, b) => a.length === b.length && a.every((val, i) => val.equals(b[i])),
};

const arr1 = [new Point(0, 0), new Point(1, 1)];
const arr2 = [new Point(0, 0), new Point(1, 1)];

console.log(Setoid.equals(arr1, arr2)); // true

const arr3 = [new Point(0, 0), new Point(0, 1)];
console.log(Setoid.equals(arr1, arr3)); // false

在这个例子中,我们定义了 Setoid 对象,并实现了 equals 方法来比较两个数组是否包含相同的点。我们首先创建两个具有相同坐标的点数组,并比较它们的相等性;该方法返回 true,因为两个数组具有相同的长度并且它们包含相同的点。接着,我们创建另一个点数组,并将其中一个点的坐标 y 值更改为 0;该方法返回 false,因为两个数组中的第二个元素不相等。

总之,Setoid 是一种非常有用的函数式编程概念,在 JavaScript 中可以使用它来比较对象的相等性。它可以应用于许多场景,例如测试和集合操作,可以使代码更具可读性并减少代码重复。

9、半群 (Semigroup)

在函数式编程中,Semigroup 是一种类型类(Type class)概念, 它表示具有结合性的操作。一个 Semigroup 实现必须具有 concat 方法,该方法接受另一个对象作为参数,并返回一个新的对象,表示在当前对象和传入对象之间进行“连接”或组合。

下面是一个简单的例子,其中我们定义了一个名为 Sum 的类,并实现了 concat 方法来将两个 Sum 对象相加。

class Sum {
  constructor(value) {
    this.value = value;
  }

  concat(other) {
    return new Sum(this.value + other.value);
  }
}

在这个例子中,我们首先定义了一个名为 Sum 的类,它包含一个数字值 value。然后,我们实现了 concat 方法,该方法接受另一个 Sum 对象作为参数,并返回一个新的 Sum 对象,其值为当前 Sum 对象的值与传入 Sum 对象的值之和。这里使用了 immutable 对象的概念,即每次对 Sum 对象做修改时都会创建一个新的 Sum 对象并返回它,而不是直接在原始对象上进行修改。

Semigroup 具有很多应用场景,例如在使用 JavaScript 中的许多集合(如数组、集合和字典)时,Semigroup 可以用来组合集合中的元素。下面是一个使用 Semigroup 连接两个字符串的例子:

const Semigroup = {
  concat: (a, b) => a + b,
};

const str1 = "Hello";
const str2 = " World";

console.log(Semigroup.concat(str1, str2)); // "Hello World"

在这个例子中,我们定义了 Semigroup 对象,并实现了 concat 方法来连接两个字符串。我们首先创建两个字符串,并使用 concat 方法将它们组合成一个字符串;该方法返回 "Hello World" 字符串。

总之,Semigroup 是一种非常有用的函数式编程概念,在 JavaScript 中可以使用它来组合不同类型的对象。它可以应用于许多场景,例如集合操作和数据转换等,可以使代码更具可读性并减少代码重复。

10、可折叠性 (Foldable)

在函数式编程中,可折叠性(Foldable)是一种类型类(type class)的概念,用于表示可以进行折叠(folding)或缩减(reducing)操作的数据结构。Foldable 是一种非常有用的概念,它可以使我们以函数式风格处理集合和数据结构。

在 JavaScript 中,我们可以使用 Array 类型来实现 Foldable。下面是一个示例代码:

const arr = [1, 2, 3, 4, 5];

const sum = arr.reduce((acc, cur) => acc + cur, 0);

console.log(sum); // 输出 15

在这个例子中,我们首先定义了一个数组 arr,其中包含数值 1 到 5。然后,我们使用 reduce 方法对数组元素进行缩减操作。reduce 方法接受两个参数:缩减函数和初始值。第一个参数是一个缩减函数,该函数接受两个参数,当前累积值(也称为 accumulator 或者 acc)和当前元素值(也称为 current value 或者 cur),并返回一个新的累积值。第二个参数是初始值(也称为 identity value),用于在缩减操作开始之前给定一个初始值。

在这个例子中,我们实现了一个简单的缩减函数,将累积值和当前元素相加。我们将 0 作为初始值传递给 reduce 方法。最终,reduce 方法返回 15,即所有数组元素的和。

除了 reduce 方法外,JavaScript 中还有一些其他方法可以用于 Foldable 操作,例如 map、filter 等。这些方法都可以配合 reduce 方法使用来实现更复杂的缩减操作。

可折叠性是一种非常有用的概念,在 JavaScript 中可以使用 Array 类型来实现。它可以使我们以函数式风格处理集合和数据结构,并且可以被用于广泛的场景,例如 MapReduce 操作、函数式转换等。

11、透镜 (Lens)

在函数式编程中,透镜(Lens)是一种数据操作模式。它可以让用户以不可变的方式修改对象的属性值,而不会影响原始对象。透镜模式适用于需要频繁修改对象属性值的场景,并且可以帮助我们更好地处理嵌套的数据结构。

在 JavaScript 中,我们可以使用 Ramda.js 库来实现 Lens,这个库提供了非常简单和易用的 API。下面是一个示例代码:

const R = require('ramda');

const person = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    zipCode: '10001'
  }
};

const lens = R.lensPath(['address', 'city']);
const newPerson = R.set(lens, 'Los Angeles', person);

console.log(newPerson);

在这个例子中,我们定义了一个 person 对象,其中包含名称、年龄和地址。然后,我们使用 Ramda.js 的 lensPath 方法创建了一个透镜对象,该透镜对象指定要修改的属性路径。我们将透镜对象保存在 lens 变量中。

接下来,我们使用 Ramda.js 的 set 方法来设置对象属性的值。set 方法接受三个参数:透镜对象、新的值和要修改的对象。在这个例子中,我们使用 lens 对象和新值 'Los Angeles' 来设置 person 对象的地址城市。注意,set 方法并没有直接修改原始对象,而是返回了一个新的对象 newPerson,该对象包含修改后的值。

除了 set 方法之外,Ramda.js 还提供了其他一些方法来实现透镜模式,例如 get、view、over 等。这些方法可以让我们以非常简洁和优雅的方式操作对象属性,同时保持不可变性。

总之,透镜模式是一种非常有用的数据操作模式,在 JavaScript 中可以使用 Ramda.js 库来实现。它可以帮助我们更好地处理嵌套的数据结构,并且可以使我们以非常简洁和优雅的方式修改对象属性。

12、类型签名 (Type Signatures)

通常 js 中的函数会在注释中指出参数与返回值的类型。在整个社区内存在很大的差异,但通常遵循以下模式:

// functionName :: firstArgType -> secondArgType -> returnType

// add :: Number -> Number -> Number
const add = (x) => (y) => x + y

// increment :: Number -> Number
const increment = (x) => x + 1

如果函数接受其他函数作为参数,那么这个函数需要用括号括起来。

// call :: (a -> b) -> a -> b
const call = (f) => (x) => f(x)

字符 abcd 表明参数可以是任意类型。以下版本的 map 的函数类型的参数 f,把一种类型 a 的数组转化为另一种类型 b 的数组。

// map :: (a -> b) -> [a] -> [b]
const map = (f) => (list) => list.map(f)

在函数式编程中,类型签名(Type Signatures)是指在方法或函数定义中使用特定格式的注释来描述参数和返回值的类型。这个技术被称为类型注解或类型声明。通过使用类型签名,可以使代码更加清晰、可读性更高,并且可以帮助我们在代码开发阶段快速找到潜在的错误。

在 JavaScript 中,我们可以使用 JSDoc 注释语法来声明类型签名。下面是一个示例代码:

/**
 * 计算两个数字之和
 *
 * @param {number} x 第一个数字
 * @param {number} y 第二个数字
 * @returns {number} 两个数字的和
 */
function add(x, y) {
  return x + y;
}

在这个例子中,我们使用 JSDoc 注释语法来声明了这个 add 函数的类型签名。我们使用 @param 标记来声明函数参数的类型,@returns 标记来声明函数返回值的类型。在这个例子中,我们声明了两个数字类型的参数,返回值也是数字类型。

使用类型签名的好处是它可以让其他人更容易理解代码以及如何使用它。此外,在实现复杂的功能时,你可能会创建自己的类型,这可以帮助你确保正确传递数据类型并捕获错误,从而提高代码的质量和稳定性。

总之,类型签名是一种非常有用的函数式编程技术,可以帮助我们编写更好的代码。在 JavaScript 中,可以使用 JSDoc 注释语法来声明类型签名,从而使代码更加清晰、易读,并且提高代码的质量和可靠性。

推荐阅读:

13、代数数据类型 (Algebraic data type)

在函数式编程中,代数数据类型(Algebraic Data Type, ADT)是一种用于组合其他数据类型的抽象数据类型。ADT 是由两个基本概念构成的:和类型(Sum type)和积类型(Product type)。和类型表示一个值可以取多个类型中的一种,而积类型则表示一个值由多个值组合而成。

在 JavaScript 中,虽然它本身没有提供代数数据类型的支持,但我们可以使用对象或数组模拟它们。下面是一个示例代码:

// 和类型示例 - 表示要么是字符串要么是数字
type StringOrNumber = string | number;

// 积类型示例 - 表示一个人员具有姓名、年龄和地址属性
type Person = {
  name: string;
  age: number;
  address: string;
};

// 组合类型示例 - 表示一个学生或老师,具有相同的姓名和年龄属性,但不同的地址属性
type StudentOrTeacher = {
  name: string;
  age: number;
  address: string;
  role: 'student' | 'teacher';
};

在这个例子中,我们定义了三个不同的代数数据类型,分别用 TypeScript 的语法声明。StringOrNumber 是一个和类型,表示一个值可以是字符串或数字类型。Person 是一个积类型,表示一个人员具有姓名、年龄和地址属性。StudentOrTeacher 是一个组合类型,表示一个学生或老师,具有相同的姓名和年龄属性,但不同的地址属性。

使用代数数据类型有助于我们在代码中更加准确地表示数据结构,并且避免出现一些潜在的错误。例如,在上面的例子中,由于 StudentOrTeacher 中定义了 role 属性,因此我们可以杜绝了一些潜在的错误。

总之,代数数据类型是一种非常强大的函数式编程技术,它提供了一种抽象和组合其他数据类型的方式。在 JavaScript 中,我们可以通过对象或数组模拟它们,并且它们能够帮助我们更精确地表示复杂数据结构,使我们更好地进行类型检查,并杜绝潜在的错误。

13.1 和类型 (Sum type)

和类型是将两种类型组合成另一种类型。之所以称为和,是因为结果类型的可能的值的数目是两种输入类型的值的数目的和。

js 中没有这种类型,但是我们可以用 set 来假装:

// 想象这些不是 set,而是仅包含这些值的某种类型。
const bools = new Set([true, false])
const halfTrue = new Set(['half-true'])

// 这个 weakLogic 类型包含 bools 类型和 halfTrue 类型的和。
const weakLogicValues = new Set([...bools, ...halfTrue])

和类型有时也称作联合类型(union type)、区分联合(discriminated union)或标记联合(tagged unions)。

JS中有一些库可以帮助定义和使用联合类型。

流(flow)包括联合类型,而TypeScript具有提供相同能力的枚举(enum)。

和类型也被称为联合类型,表示一个值可以是多个类型中的一种。在 JavaScript 中,我们可以使用联合类型或者枚举类型来模拟和类型。

下面是一个示例代码:

// 使用联合类型模拟和类型
function formatValue(value: string | number) {
  if (typeof value === 'string') {
    return `"${value}"`;
  } else {
    return value.toFixed(2);
  }
}

// 使用枚举类型模拟和类型
enum PaymentMethod {
  CreditCard,
  PayPal,
  Venmo,
  ApplePay
}

type Order = {
  id: string;
  amount: number;
  paymentMethod: PaymentMethod;
}

function processOrder(order: Order) {
  switch (order.paymentMethod) {
    case PaymentMethod.CreditCard:
      // 处理信用卡支付
      break;
    case PaymentMethod.PayPal:
      // 处理 PayPal 支付
      break;
    case PaymentMethod.Venmo:
      // 处理 Venmo 支付
      break;
    case PaymentMethod.ApplePay:
      // 处理 Apple Pay 支付
      break;
    default:
      throw new Error('Unsupported payment method');
  }
}

在这个例子中,我们分别使用联合类型和枚举类型来模拟和类型。formatValue 函数接受一个字符串或数字类型的值,并根据其类型返回不同的格式化结果。而 processOrder 函数则接受一个包含订单信息的对象,其中 paymentMethod 属性可以是四种不同的支付方式,我们通过 switch 语句来根据不同的支付方式进行不同的处理。

和类型在函数式编程中还有其他应用,例如代数数据类型。它可以帮助我们更精确地表示复杂的数据结构,并避免出现潜在的错误。例如,当我们需要处理一个值可以是多种类型中的一种时,可以使用和类型来表示这个值的类型。

13.2 积类型(Product type)

用一种你可能更熟悉的方式把数据类型联合起来:

// point :: (Number, Number) -> {x: Number, y: Number}
const point = (x, y) => ({x: x, y: y})

之所以称之为积,是因为数据结构的总的可能值是不同值的乘积。许多语言都有 tuple 类型,这是积类型的最简单形式。

积类型也被称为元组类型或产品类型,表示一个值是多个类型的组合。在 JavaScript 中,我们可以使用数组或者对象来模拟积类型。

下面是一个示例代码:

// 使用数组模拟积类型
function calculateTotal(items) {
  let subtotal = 0;
  for (let i = 0; i < items.length; i++) {
    subtotal += items[i].price * items[i].quantity;
  }
  return [subtotal, subtotal * 0.2, subtotal * 1.2];
}

const items = [
  { name: 'apple', price: 0.5, quantity: 10 },
  { name: 'orange', price: 0.7, quantity: 8 },
  { name: 'banana', price: 0.3, quantity: 15 }
];

const [subtotal, tax, total] = calculateTotal(items);
console.log(`Subtotal: ${subtotal}, Tax: ${tax}, Total: ${total}`);

// 使用对象模拟积类型
type Point2D = {
  x: number;
  y: number;
}

type Circle = {
  center: Point2D;
  radius: number;
}

function getCircleArea(circle: Circle): number {
  return Math.PI * circle.radius ** 2;
}

const myCircle = {
  center: { x: 0, y: 0 },
  radius: 5
};

console.log(getCircleArea(myCircle));

在这个例子中,我们分别使用数组和对象来模拟积类型。calculateTotal 函数接受一个包含商品信息的数组,并返回一个包含小计、税金和总价的数组。我们使用解构赋值来将这个数组拆成各个部分,并打印出结果。而 getCircleArea 函数接受一个包含圆心坐标和半径的对象,计算并返回圆的面积。

在函数式编程中,我们还可以使用积类型来表示代数数据类型中的产品类型。例如,在 TypeScript 中,我们可以使用 interface 或 type alias 来定义产品类型:

// 定义一个包含姓名和年龄的人类
interface Person {
  name: string;
  age: number;
}

// 定义一个包含商品名称和价格的商品类
type Product = {
  name: string;
  price: number;
}

在这个例子中,Person 和 Product 都是代数数据类型中的产品类型,分别表示一个人和一个商品的信息。

推荐阅读:

14、可选类型 (Option)

Option 是一种联合类型,它有两种情况,Some 或者 None。Option对于一些可能不会返回值的组合函数非常有用。

// 简单的定义
const Some = (v) => ({
  val: v,
  map (f) {
    return Some(f(this.val))
  },
  chain (f) {
    return f(this.val)
  }
})

const None = () => ({
  map (f) {
    return this
  },
  chain (f) {
    return this
  }
})

// maybeProp :: (String, {a}) -> Option a
const maybeProp = (key, obj) => typeof obj[key] === 'undefined' ? None() : Some(obj[key])

使用 chain 可以序列化返回 Option 的函数。

// getItem :: Cart -> Option CartItem
const getItem = (cart) => maybeProp('item', cart)

// getPrice :: Item -> Option Number
const getPrice = (item) => maybeProp('price', item)

// getNestedPrice :: cart -> Option a
const getNestedPrice = (cart) => getItem(obj).chain(getPrice)

getNestedPrice({}) // None()
getNestedPrice({item: {foo: 1}}) // None()
getNestedPrice({item: {price: 9.99}}) // Some(9.99)

在其它的一些地方,Option 也称为 MaybeSome 也称为 JustNone 也称为 Nothing

在函数式编程中,可选类型也被称为 Maybe 类型或 Option 类型,表示一个值可能存在,也可能不存在。在 TypeScript 中,我们可以使用联合类型或者泛型来实现可选类型。

以下是一个示例代码:

// 使用联合类型实现可选类型
interface User {
  name: string;
  age?: number; // 可选属性
}

function getUserName(user: User): string {
  return user.name;
}

const user1 = { name: 'Alice' };
const user2 = { name: 'Bob', age: 30 };

console.log(getUserName(user1)); // Alice
console.log(getUserName(user2)); // Bob

// 使用泛型实现可选类型
type Option<T> = T | null | undefined;

interface Product {
  name: string;
  price: number;
  description?: string; // 可选属性
}

function getProductDescription(product: Product): Option<string> {
  return product?.description ?? null; // 使用空值合并运算符 ?? 来处理 undefined 和 null 的情况
}

const product1 = { name: 'apple', price: 0.5 };
const product2 = { name: 'orange', price: 0.7, description: 'A juicy fruit' };

console.log(getProductDescription(product1)); // null
console.log(getProductDescription(product2)); // A juicy fruit

在这个例子中,我们分别使用联合类型和泛型来实现可选类型。getUserName 函数接受一个包含姓名和年龄的对象,返回姓名。我们将年龄属性标记为可选属性,在使用时需要判断其是否存在。而 getProductDescription 函数接受一个包含商品信息的对象,返回描述信息。我们使用泛型 Option 来处理可能不存在的情况,如果存在则返回该属性的值,否则返回 null。

函数式编程中的可选类型非常有用,可以避免在访问不存在的属性或者调用不存在的方法时出现错误,从而提高程序的健壮性。同时,使用可选类型也可以使代码更加简洁易懂。

15、Function

一个函数 f :: A => B 是一个表达式,通常称为 arrow 或者 lambda 表达式——只能有一个(这点是不可变的)的 A 类型参数和一个 B 类型返回值。该返回值完全取决于参数,使函数独立于上下文,或者说引用透明。这里暗示的是一个函数不能产生任何隐藏的副作用——根据定义,函数总是的。这些属性使函数易于使用:它们是完全确定的,因此也是可以预测的。函数可以将代码作为数据进行处理,对行为进行抽象:

// times2 :: Number -> Number
const times2 = n => n * 2

[1, 2, 3].map(times2) // [2, 4, 6]

在函数式编程中,函数是一等公民,可以像值一样被赋值、传递和返回。Function 类型在 JavaScript 中也同样具有这样的特点。

以下是一个示例代码:

// 函数类型定义
type UnaryFunction<T, R> = (arg: T) => R;

// 函子类型定义
interface Functor<T> {
  map<R>(fn: UnaryFunction<T, R>): Functor<R>;
}

// Maybe 函子实现
class Maybe<T> implements Functor<T>{
  constructor(private readonly value: T) {}

  map<R>(fn: UnaryFunction<T, R>): Functor<R> {
    if (!this.value) {
      return new Maybe(null);
    }
    const newValue = fn(this.value);
    return new Maybe(newValue);
  }
}

// 使用 Maybe 函子处理可能不存在的属性值
const user = {
  name: 'Alice',
  age: 30,
  address: {
    city: 'Shanghai'
  }
};

const userName = new Maybe(user)
   .map(u => u.name)
   .map(n => n.toUpperCase())
   .map(s => `Hello, ${s}!`)
   .map(console.log); // 打印 "Hello, ALICE!"

const userCity = new Maybe(user)
   .map(u => u.address)
   .map(a => a.city)
   .map(console.log); // 打印 "Shanghai"

在这个例子中,首先我们定义了一个函数类型 UnaryFunction 和函子类型 Functor。然后实现了一个 Maybe 函子,它用来处理可能不存在的属性值。我们将一个包含用户信息的对象传入 Maybe 函子,并依次调用 map 方法来处理该对象的姓名和地址信息。当属性值不存在时,Maybe 函子返回一个包含 null 值的 Maybe 实例,这样后续的操作都不会产生错误。

函数式编程中的 Function 类型与普通的 JavaScript 函数相比,在应用上更加灵活。我们可以通过组合多个函数来实现复杂的逻辑,同时避免了副作用和状态变化等问题,使得代码更加简洁易懂,从而提高代码的可维护性和健壮性。

16、偏函数 (Partial function)

偏函数是一种常见的函数式编程技术,它可以创建一个新的函数,该函数仅绑定部分参数而不是全部参数。这个新的函数可以被反复调用,每次传入不同的缺失参数,从而得到不同的结果。

下面是一个简单的偏函数示例:

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

const add10 = add.bind(null, 10);
const res1 = add10(20, 30); // 60

const add20 = add.bind(null, 20);
const res2 = add20(30, 40); // 90

在这个例子中,我们定义了一个接收三个参数的 add 函数,在全局作用域下使用 bind 方法创建了两个偏函数 add10 和 add20。这两个偏函数分别绑定了第一个参数为 10 和 20,返回了新函数,并将其赋值给变量。之后我们可以多次调用这些偏函数,并传递剩余的参数来计算不同的结果。

偏函数在实际开发中也有广泛的应用。例如,我们经常需要将一个具有多个参数的复杂函数进行拆分或者组合,以便于代码重用和测试。偏函数就是实现这个目标的有效工具之一。

下面是一个使用偏函数实现组合函数的例子:

function compose(...fns) {
  return function(result) {
    return fns.reduceRight(function(result, fn) {
      return fn(result);
    }, result);
  };
}

function split(separator, str) {
  return str.split(separator);
}

function join(separator, arr) {
  return arr.join(separator);
}

const splitByComma = split.bind(null, ',');
const joinBySemicolon = join.bind(null, ';');

const transform = compose(splitByComma, joinBySemicolon);

const str = 'a,b,c,d';
const res = transform(str); // "a;b;c;d"

在这个例子中,我们实现了一个函数式编程中常见的组合函数。该函数接收多个函数作为参数,然后返回一个新的函数,该新函数将多个函数依次执行,并将结果传递给下一个函数,最终输出最终结果。我们使用 bind 方法创建了两个偏函数 splitByComma 和 joinBySemicolon。这些偏函数仅绑定了部分参数,并返回了新函数。最后,我们将这些函数应用于实际的字符串值,并得到了处理过的结果。

总结来说,偏函数是一种强大而灵活的技术,它可以提高代码重用性和可维护性,同时减少开发中可能出现的错误和副作用等问题。因此,在函数式编程中,经常会使用偏函数来进行函数的拆分、组合和变换,并用于各种类型的业务逻辑中。

17、函数式编程库推荐

end~