我发现自己一遍又一遍,正在写这样的代码,并且在思考。必须有一个已知的模式,但是要仔细研究Ramda等不同功能库的文档。我找不到比赛。我该怎么用?
var arrayOfPersons = [{ firstName: 'Jesper', lastName: 'Jensen', income: 120000, member: true }/* .... a hole lot of persons */];
function createPredicateBuilder(config) {
return {
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.
},
map(newConfig) {
return createPredicateBuilder({ ...config, ...newConfig });
}
};
}
var predicateBuilder = createPredicateBuilder({});
// We collect predicates
predicateBuilder = predicateBuilder.map({ minIncome: 200000 });
// ...
predicateBuilder = predicateBuilder.map({ member: false });
// Now we want to query...
console.log(arrayOfPersons.filter(predicateBuilder.build()));
我创建一个构建器实例,并调用map,并使用build / map方法在对象中返回一个新实例。状态在功能范围中捕获。 将来的某个时候,我想获取我收集的函数(或结果)。
我认为这是FP,但是这种模式是什么,是否有任何库使它更容易?
我的灵感源自事物的名称(构建者/构建者)使我蒙昧吗?
答案 0 :(得分:3)
您可以使用Ramda中的where
函数对描述您的谓词的spec对象进行测试。然后,您的代码可以根据传递的配置动态地构建spec对象。
https://ramdajs.com/docs/#where
Ramda文档中的示例:
// pred :: Object -> Boolean
const pred = R.where({
a: R.equals('foo'),
b: R.complement(R.equals('bar')),
x: R.gt(R.__, 10),
y: R.lt(R.__, 20)
});
pred({a: 'foo', b: 'xxx', x: 11, y: 19}); //=> true
pred({a: 'xxx', b: 'xxx', x: 11, y: 19}); //=> false
pred({a: 'foo', b: 'bar', x: 11, y: 19}); //=> false
pred({a: 'foo', b: 'xxx', x: 10, y: 19}); //=> false
pred({a: 'foo', b: 'xxx', x: 11, y: 20}); //=> false
要详细说明,您可以通过具有一组函数来“构建” spec对象,这些函数可以返回带有附加谓词的新spec,例如:
function setMinIncome(oldSpec, minIncome) {
return R.merge(oldSpec, {income: R.gt(R.__, minIncome)})
}
答案 1 :(得分:2)
这是Builder design pattern。尽管它已在一种更实用的方法中进行了更改,但前提保持不变-您拥有一个实体,该实体通过.map()
收集信息(更传统的是.withX()
对应于setter)并执行所有收集的数据,新对象.build()
。
为了使这一点更容易识别,下面是一种更面向对象的方法,该方法仍然可以执行相同的操作:
class Person {
constructor(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
toString() {
return `I am ${this.firstName} ${this.lastName} and I am ${this.age} years old`;
}
}
class PersonBuilder {
withFirstName(firstName) {
this.firstName = firstName;
return this;
}
withLastName(lastName) {
this.lastName = lastName;
return this;
}
withAge(age) {
this.age = age;
return this;
}
build() {
return new Person(this.firstName, this.lastName, this.age);
}
}
//make builder
const builder = new PersonBuilder();
//collect data for the object construction
builder
.withFirstName("Fred")
.withLastName("Bloggs")
.withAge(42);
//build the object with the collected data
const person = builder.build();
console.log(person.toString())
答案 2 :(得分:2)
功能编程与模式无关,而与法律有关。法律使程序员可以像数学家可以对方程式那样对程序进行推理。
让我们看一下加数。加法是一个二进制运算(它需要两个个数字),并且总是产生另一个数字。
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
在任何数字上添加零不会更改数字。这就是 identity元素。
这两个东西( 1 )是一个关联的二进制运算,而( 2 )是一个标识元素,组成了 monoid 。
>如果可以的话...
...然后我们得到属于monoid类别的好处,使我们能够以 equalal 的方式推理程序。没有学习的模式,只有遵守的法律。
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具有对函数的 first-class 支持,这意味着可以将它们视为值。返回函数或将函数作为输入的程序称为高阶函数。
以上,我们的谓词表示为具有任意类型( a )值并产生 boolean 的函数。我们将其表示为a -> Boolean
。因此,每个谓词都是我们领域的一个元素,而该领域就是所有功能a -> Boolean
。
2。二进制运算
我们将再进行一次抽象练习。让我们采用静态组合谓词表达式。
p1 (item) && p2 (item)
我可以通过将该表达式包装在函数中并将item
设置为函数的参数来将该表达式用于其他项目。
const bothPredicates = item =>
p1 (item) && p2 (item)
但是我们希望能够组合任何谓词。再次,我们将要重复使用的表达式包装在函数中,然后为变量分配参数,这次是p1
和p2
。
const and = (p1, p2) => item =>
p1 (item) && p2 (item)
在继续之前,让我们检查域并确保二进制操作and
是正确的。二进制操作必须:
a -> Boolean
)中的两(2)个元素为输入实际上,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。身份元素
identity元素添加到任何其他元素后,不得更改该元素。因此,对于任何谓词p
和谓词身份元素empty
,必须满足以下条件
和(p,空)== p
和(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
)
法律权力
现在我们有了一个二进制运算和一个identity元素,我们可以组合任意数量的谓词。我们定义了sum
,它将我们的Monoid直接插入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
)
免费便利
使用函数对谓词进行编码也使它们在其他方面也很有用。如果您有一组项目,则可以直接在p
或.find
中使用谓词.filter
。当然对于使用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。
答案 3 :(得分:0)
我坚持使用(组成)谓词函数的简单数组和任一函数的归约器
And
(f => g => x => f(x) && g(x)
),种子是True
(_ => true
)。 Or
(f => g => x => f(x) || g(x)
),种子是False
(_ => false
)。例如:
const True = _ => true;
const False = _ => false;
const Or = (f, g) => x => f(x) || g(x);
Or.seed = False;
const And = (f, g) => x => f(x) && g(x);
And.seed = True;
const Filter = (fs, operator) => fs.reduce(operator, operator.seed);
const oneOrTwo =
Filter([x => x === 1, x => x === 2], Or);
const evenAndBelowTen =
Filter([x => x % 2 === 0, x => x < 10], And);
const oneToHundred = Array.from(Array(100), (_, i) => i);
console.log(
"One or two",
oneToHundred.filter(oneOrTwo),
"Even and below 10",
oneToHundred.filter(evenAndBelowTen)
);
您甚至可以通过嵌套And
/ Or
结构来创建复杂的过滤器逻辑:
const True = _ => true;
const False = _ => false;
const Or = (f, g) => x => f(x) || g(x);
Or.seed = False;
const And = (f, g) => x => f(x) && g(x);
And.seed = True;
const Filter = (fs, operator) => fs.reduce(operator, operator.seed);
const mod = x => y => y % x === 0;
const oneToHundred = Array.from(Array(100), (_, i) => i);
console.log(
"Divisible by (3 and 5), or (3 and 7)",
oneToHundred.filter(
Filter(
[
Filter([mod(3), mod(5)], And),
Filter([mod(3), mod(7)], And)
],
Or
)
)
);
或者,以您自己的示例情况:
const comp = (f, g) => x => f(g(x));
const gt = x => y => y > x;
const eq = x => y => x === y;
const prop = k => o => o[k];
const And = (f, g) => x => f(x) && g(x);
const True = _ => true;
const Filter = (fs) => fs.reduce(And, True);
const richMemberFilter = Filter(
[
comp(gt(200000), prop("income")),
comp(eq(true), prop("member"))
]
);
console.log(
"Rich members:",
data().filter(richMemberFilter).map(prop("firstName"))
);
function data() {
return [
{ firstName: 'Jesper', lastName: 'Jensen', income: 120000, member: true },
{ firstName: 'Jane', lastName: 'Johnson', income: 230000, member: true },
{ firstName: 'John', lastName: 'Jackson', income: 230000, member: false }
];
};