【问题标题】:What functional-pattern is this? If any?这是什么功能模式?如果有的话?
【发布时间】:2018-12-11 07:39:18
【问题描述】:

我发现自己一遍又一遍地编写这样的代码,并且在思考。对此必须有一个已知的模式,但要通过不同功能库(如 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,但这种模式是什么,是否有任何库可以使它更容易?

我对事物 (builder/build) 的 oop 启发命名是否让我眼花缭乱?

【问题讨论】:

  • 我不是 FP 方面的专家,但我认为您可以使用 Monad 而不是 predicateBuilder。
  • 它不是组合函数,而是更多地收集部分并将它们存储在范围内。那么除了用 monad 组成 fpPredicate 之外,你会如何使用 monad 来收集呢?
  • 构建器模式是一种 OOP 设计模式,AFAIK 用于缓解产品类型(如对象)的缺点之一。产品类型是分层的,也就是说,您只能添加信息并以大量 null 值和大量处理成员变量的所有组合的构造函数结束。当然,你可以避免this。并且你可以通过依赖某些函数的调用堆栈来减少突变,但本质上它仍然是一种 OOP 模式,在 FP 中没有它的名称。

标签: javascript design-patterns functional-programming


【解决方案1】:

您可以使用 Ramda 中的 where 函数来针对描述您的谓词的规范对象进行测试。然后,您的代码可以根据传递的配置动态构建规范对象。

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

更详细地说,您可以通过一组函数来“构建”规范对象,这些函数返回带有附加谓词的新规范,例如:

function setMinIncome(oldSpec, minIncome) {
  return R.merge(oldSpec, {income: R.gt(R.__, minIncome)})
}

【讨论】:

  • 我看到你要去哪里了,我看了R.where和R.memoize,但是不太一样。我的示例正在根据参数“构建”谓词。
  • 是的,我明白你在说什么。唯一缺少的是“覆盖”规范的能力。我的“建设者”将在范围内包含规范部分,并覆盖它们,但我想我会按照你所指的路径尝试一些事情。
  • 如何以函数方式更新对象(即返回一个新对象)当然是一个话题。例如。你可以使用镜头
【解决方案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

将零添加到任何数字都不会改变数字。这被称为身份元素


这两个东西,(1) 一个关联二元运算和 (2) 一个标识元素,组成了一个 monoid。 p>

如果可以的话……

  1. 将谓词编码为域的元素
  2. 为元素创建二元运算
  3. 确定身份元素

...然后我们获得了属于幺半群类别的好处,使我们能够以等式的方式推理我们的程序。没有要学习的模式,只有要维护的规律。


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 具有对函数的一流支持,这意味着它们可以被视为值。返回函数或接收函数作为输入的程序称为高阶函数

在上面,我们的谓词表示为函数,它接受任何类型的值 (a) 并产生一个 boolean。我们将其表示为a -&gt; Boolean。所以每个谓词都是我们域的一个元素,而那个域是所有函数a -&gt; Boolean


2。二元运算

我们将再做一次抽象练习。我们来看一个静态组合谓词表达式。

p1 (item) && p2 (item)

我可以将此表达式重新用于其他项目,方法是将其包装在一个函数中并将item 作为函数的参数。

const bothPredicates = item =>
  p1 (item) && p2 (item)

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

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

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

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

确实,and 接受我们域 p1p2 的两个元素。返回值为item =&gt; ...,这是一个接收item 并返回p1 (item) &amp;&amp; p2 (item) 的函数。每个都是接受单个值并返回布尔值的谓词。这简化为Boolean &amp;&amp; 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。当然,对于使用 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)

测验

注意我们的谓词标识元素和&amp;&amp;的标识元素之间的相似性

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】:

    这是Builder design pattern。尽管它以更实用的方法进行了更改,但前提保持不变 - 您有一个实体通过 .map() 收集信息(更传统的是 .withX() 对应于 setter)并执行所有收集的数据,生成一个新对象 @987654324 @。

    为了让这一点更容易识别,这里有一个更面向对象的方法,它仍然做同样的事情:

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

    【讨论】:

    • 我想要 FP 中的构建器设计模式 - 所以没有状态突变和对此的引用。
    • @JesperJensen 这是 Builder 模式的更“传统”实现。我所说的“传统”是指面向对象,因为它更容易从其他文献中识别出来。没有什么能阻止你做你正在做的事情,我认为它是相同的模式,尽管它没有使用与 OO 代码完全相同的语法。重点是延迟构造 something(它不必是类实例),让您有机会在此之前收集数据。
    【解决方案4】:

    我会坚持使用一个简单的(组合)谓词函数数组和两者之一的化简器

    • And (f =&gt; g =&gt; x =&gt; f(x) &amp;&amp; g(x)),以 True (_ =&gt; true) 为种子。
    • Or (f =&gt; g =&gt; x =&gt; f(x) || g(x)),播种 False (_ =&gt; 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 }
      ]; 
    };

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-10-01
      • 2021-08-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多