本篇接上一篇【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表达式实现了两个函数 add
和 mul
,它们分别表示加法和乘法的函数抽象。然后,我们使用函数组合的方式将它们组成一个表达式,并计算结果。
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)是指一个满足以下条件的数学结构:
存在一个元素(称为幺元),使任何元素与之结合得到自身
该结构满足结合律:任意三个元素 a、b、c,满足 (a+b)+c=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) }
在上述代码中,我们分别定义了一个数字加法幺半群和一个字符串拼接幺半群,并实现了它们的 empty
和 concat
方法。这两个方法分别对应了幺半群的幺元和结合律。
除此之外,在函数式编程中,我们也经常使用幺半群来实现一些高阶函数。例如,下面的代码演示了如何使用幺半群结合 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 函数,它接收一个数组作为参数,并使用幺半群的 concat 和 empty 方法来计算数组元素的和。
另外,幺半群还可以用于函数组合。例如,下面的示例代码演示了如何使用幺半群实现一个 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中,单子的标准实现方式是通过一个类来实现,这个类需要满足以下条件:
- 包含一个值
- 声明一个
of
静态方法,用于创建一个新的单子 - 实现一个
map
方法,用于对单子中的值进行变换 - 实现一个
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中,余单子的标准实现方式是通过一个类来实现,这个类需要满足以下条件:
- 包含一个值
- 实现一个
extract
方法,用于提取出值中的元素 - 实现一个
map
方法,用于对余单子中的值进行变换 - 实现一个
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
在上述代码中,我们定义了三个自同态函数 double、addOne 和 square。其中,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)或到达指定的深度。在这个例子中,apo
和 Futu
的迭代方式使得我们可以轻松地生成任何可逆数据结构,而无需编写许多重复代码。
Apomorphism
是一种非常强大的组合原语,在函数式编程中具有广泛的应用。可以使用 Apomorphism 来从初始值生成可逆数据结构,并基于该数据结构构建新的数据结构。这使得 Apomorphism
成为解决许多复杂问题的有力工具。