这是什么功能模式?如果有的话

时间:2018-12-11 07:39:18

标签: javascript design-patterns functional-programming

我发现自己一遍又一遍,正在写这样的代码,并且在思考。必须有一个已知的模式,但是要仔细研究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,但是这种模式是什么,是否有任何库使它更容易?

我的灵感源自事物的名称(构建者/构建者)使我蒙昧吗?

4 个答案:

答案 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

>

如果可以的话...

  1. 将谓词编码为域的元素
  2. 为元素创建二进制操作
  3. 确定身份元素

...然后我们得到属于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.
},

...我们看到了程序级别和数据级别的混合。该程序经过硬编码,只能理解可能具有这些特定键(minIncomemember)及其各自类型(numberboolean)的输入用于确定谓词的比较操作。

让我们保持非常简单。我们来一个静态谓词

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)

但是我们希望能够组合任何谓词。再次,我们将要重复使用的表达式包装在函数中,然后为变量分配参数,这次是p1p2

const and = (p1, p2) => item =>
  p1 (item) && p2 (item)

在继续之前,让我们检查域并确保二进制操作and是正确的。二进制操作必须:

  1. 以我们域(a -> Boolean)中的两(2)个元素为输入
  2. 将域元素返回作为输出
  3. 该操作必须是关联的: f(a,b) == f(b,a)

实际上,and接受域p1p2的两个元素。返回值为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。当然对于使用andsum创建的谓词也是如此。

const p =
  sum (pred1, pred2, pred3, ...)

const items =
  [ { name: "Alice" ... }
  , { name: "Sally" ... }
  ]

const firstMatch =
  items .find (p)

const allMatches =
  items .filter (p)

将其设为模块

您不想定义addsumempty之类的全局变量。打包此代码时,请使用某种模块。

// 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)

我坚持使用(组成)谓词函数的简单数组和任一函数的归约器

  • Andf => g => x => f(x) && g(x)),种子是True_ => true)。
  • Orf => 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 }
  ]; 
};