函数式编程不是关于模式,而是更多关于规律。定律允许程序员对他们的程序进行推理,就像数学家对方程进行推理一样。
让我们看看添加数字。添加是一个二元运算(它需要两个数字)并且总是产生另一个数字。
1 + 2 = 3
2 + 1 = 3
1 + (2 + 3) = 6
(1 + 2) + 3 = 6
((1 + 2) + 3) + 4 = 10
(1 + 2) + (3 + 4) = 10
1 + (2 + 3) + 4 = 10
1 + (2 + (3 + 4)) = 10
我们可以按任何顺序添加数字,但仍然得到相同的结果。该属性是结合性,它构成了结合律的基础。
加零有点有趣,或者被认为是理所当然的。
1 + 0 = 1
0 + 1 = 1
3 + 0 = 3
0 + 3 = 3
将零添加到任何数字都不会改变数字。这被称为身份元素。
这两个东西,(1) 一个关联二元运算和 (2) 一个标识元素,组成了一个 monoid。 p>
如果可以的话……
- 将谓词编码为域的元素
- 为元素创建二元运算
- 确定身份元素
...然后我们获得了属于幺半群类别的好处,使我们能够以等式的方式推理我们的程序。没有要学习的模式,只有要维护的规律。
1.制作域名
获取正确的数据非常棘手,在 JavaScript 这样的多范式语言中更是如此。这个问题是关于函数式编程的,所以 functions 是一个不错的选择。
在你的程序中...
build() {
var fnPredicate = (p) => true;
if (typeof config.minIncome == 'number') {
fnPredicate = (p) => fnPredicate(p) && config.minIncome <= p.income;
}
if (typeof config.member == 'boolean') {
fnPredicate = (p) => fnPredicate(p) && config.member === p.member;
}
// .. continue to support more predicateparts.
},
...我们看到了程序级别和数据级别的混合。这个程序是硬编码的,只能理解可能具有这些特定键(minIncome、member)及其各自类型(number 和 boolean)的输入,以及用于确定谓词的比较操作.
让我们保持简单。让我们来一个静态谓词
item.name === "Sally"
如果我想要相同的谓词但使用不同的项目进行比较,我会将此表达式包装在一个函数中,并将item 作为函数的参数。
const nameIsSally = item =>
item.name === "Sally"
console .log
( nameIsSally ({ name: "Alice" }) // false
, nameIsSally ({ name: "Sally" }) // true
, nameIsSally ({ name: "NotSally" }) // false
, nameIsSally ({}) // false
)
这个谓词很容易使用,但它只适用于检查名字Sally。我们通过将表达式包装在一个函数中来重复该过程,并将name 作为函数的参数。这种通用技术称为抽象,它一直用于函数式编程。
const nameIs = name => item =>
item.name === name
const nameIsSally =
nameIs ("Sally")
const nameIsAlice =
nameIs ("Alice")
console .log
( nameIsSally ({ name: "Alice" }) // false
, nameIsSally ({ name: "Sally" }) // true
, nameIsAlice ({ name: "Alice" }) // true
, nameIsAlice ({ name: "Sally" }) // false
)
如您所见,我们包装的表达式已经是一个函数并不重要。 JavaScript 具有对函数的一流支持,这意味着它们可以被视为值。返回函数或接收函数作为输入的程序称为高阶函数。
在上面,我们的谓词表示为函数,它接受任何类型的值 (a) 并产生一个 boolean。我们将其表示为a -> Boolean。所以每个谓词都是我们域的一个元素,而那个域是所有函数a -> Boolean。
2。二元运算
我们将再做一次抽象练习。我们来看一个静态组合谓词表达式。
p1 (item) && p2 (item)
我可以将此表达式重新用于其他项目,方法是将其包装在一个函数中并将item 作为函数的参数。
const bothPredicates = item =>
p1 (item) && p2 (item)
但我们希望能够组合 any 谓词。同样,我们将要重复使用的表达式包装在一个函数中,然后为变量分配参数,这次是 p1 和 p2。
const and = (p1, p2) => item =>
p1 (item) && p2 (item)
在我们继续之前,让我们检查一下我们的域并确保我们的二进制操作and 是正确的。二元运算必须:
- 将我们域 (
a -> Boolean) 中的两 (2) 个元素作为输入
- 将我们域的元素作为输出返回
- 操作必须是关联的:f(a,b) == f(b,a)
确实,and 接受我们域 p1 和 p2 的两个元素。返回值为item => ...,这是一个接收item 并返回p1 (item) && p2 (item) 的函数。每个都是接受单个值并返回布尔值的谓词。这简化为Boolean && Boolean,我们知道它是另一个布尔值。总而言之,and 接受两个谓词并返回一个新谓词,这正是二元运算必须做的。
const and = (p1, p2) => item =>
p1 (item) && p2 (item)
const nameIs = x => item =>
item.name === x
const minIncome = x => item =>
x <= item.income
const query =
and
( nameIs ("Alice")
, minIncome (5)
)
console .log
( query ({ name: "Sally", income: 3}) // false
, query ({ name: "Alice", income: 3 }) // false
, query ({ name: "Alice", income: 7 }) // true
)
3.身份元素
标识元素在添加到任何其他元素时,不得更改该元素。因此对于任何谓词p 和谓词标识元素empty,以下必须成立
and (p, empty) == p
and (empty, p) == p
我们可以将空谓词表示为一个接受任何元素的函数,并且总是返回true。
const and = (p1, p2) => item =>
p1 (item) && p2 (item)
const empty = item =>
true
const p = x =>
x > 5
console .log
( and (p, empty) (3) === p (3) // true
, and (empty, p) (3) === p (3) // true
)
法律的力量
现在我们有了一个二元运算和一个恒等元素,我们可以组合任意数量的谓词。我们定义了sum,它将我们的幺半群直接插入reduce。
// --- predicate monoid ---
const and = (p1, p2) => item =>
p1 (item) && p2 (item)
const empty = item =>
true
const sum = (...predicates) =>
predicates .reduce (and, empty) // [1,2,3,4] .reduce (add, 0)
// --- individual predicates ---
const nameIs = x => item =>
item.name === x
const minIncome = x => item =>
x <= item.income
const isTeenager = item =>
item.age > 12 && item.age < 20
// --- demo ---
const query =
sum
( nameIs ("Alice")
, minIncome (5)
, isTeenager
)
console .log
( query ({ name: "Sally", income: 8, age: 14 }) // false
, query ({ name: "Alice", income: 3, age: 21 }) // false
, query ({ name: "Alice", income: 7, age: 29 }) // false
, query ({ name: "Alice", income: 9, age: 17 }) // true
)
空和谓词仍然返回有效结果。这就像匹配所有结果的空查询。
const query =
sum ()
console .log
( query ({ foo: "bar" }) // true
)
免费方便
使用函数来编码我们的谓词使它们在其他方面也有用。如果您有一个项目数组,您可以直接在.find 或.filter 中使用谓词p。当然,对于使用 and 和 sum 创建的谓词也是如此。
const p =
sum (pred1, pred2, pred3, ...)
const items =
[ { name: "Alice" ... }
, { name: "Sally" ... }
]
const firstMatch =
items .find (p)
const allMatches =
items .filter (p)
将其设为模块
您不想定义像 add 和 sum 和 empty 这样的全局变量。打包此代码时,请使用某种模块。
// Predicate.js
const add = ...
const empty = ...
const sum = ...
const Predicate =
{ add, empty, sum }
export default Predicate
当你使用它时
import { sum } from './Predicate'
const query =
sum (...)
const result =
arrayOfPersons .filter (query)
测验
注意我们的谓词标识元素和&&的标识元素之间的相似性
T && ? == T
? && T == T
F && ? == F
? && F == F
我们可以用T 替换上面的所有? 并且等式将成立。下面,您认为|| 的标识元素是什么?
T || ? == T
? || T == T
F || ? == F
? || F == F
* 的标识元素是什么,二进制乘法?
n * ? = n
? * n = n
数组或列表的标识元素怎么样?
concat (l, ?) == l
concat (?, l) == l
玩得开心吗?
我想你会喜欢contravariant functors。在同一个竞技场,transducers。还有一个演示展示了如何围绕这些低级模块构建更高级别的 API。