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)
字符 a
, b
, c
, d
表明参数可以是任意类型。以下版本的 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 也称为 Maybe,Some 也称为 Just,None 也称为 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、函数式编程库推荐
- mori
- Immutable
- Immer
- Ramda
- ramda-adjunct
- Folktale
- monet.js
- lodash
- Underscore.js
- Lazy.js
- maryamyriameliamurphies.js
- Haskell in ES6
- Sanctuary
- Crocks
- Fluture
- fp-ts
end~