【问题标题】:What is the proper/idiomatic way to filter or map based on surrounding context in functional programming?在函数式编程中基于周围上下文过滤或映射的正确/惯用方法是什么?
【发布时间】:2017-08-19 02:11:33
【问题描述】:

在函数式编程中,基于单个项目的特征进行过滤相对简单——例如,过滤以仅查找奇数:

const arrayOfInfo = [1,2,3,4,5,6,8,10,11,13,15,16,17,19]

const onlyOddNumbers = arrayOfInfo.filter(function(item) {

  return (item % 2 == 1) ? true : false

})

但是,如果我需要上下文,我不确定做事的惯用方式是什么——换句话说,就是了解周围的项目。例如,如果我想只过滤两边被奇数包围的项目,我可以这样做(我正在利用一些 JavaScript 特性,甚至不用先检查索引是否存在):

const surroundedByOneOddNumber = arrayOfInfo.filter(function(item,index) {

  const itemBefore = arrayOfInfo[index - 1]
  const itemAfter = arrayOfInfo[index + 1]
  return ((itemBefore % 2 == 1) && (itemAfter % 2 == 1)) ? true : false

})

如果我想找到每边都被两个奇数包围的数字,这会成为一种有问题或效率低下的代码编写方式:

const surroundedByTwoOddNumbers = arrayOfInfo.filter(function(item,index) {

  const itemBefore = arrayOfInfo[index - 1]
  const itemTwoBefore = arrayOfInfo[index - 2]
  const itemAfter = arrayOfInfo[index + 1]
  const itemTwoAfter = arrayOfInfo[index + 2]
  return ((itemBefore % 2 == 1) && (itemTwoBefore % 2 == 1) && (itemAfter % 2 == 1) && (itemTwoAfter % 2 == 1)) ? true : false

})

显然,如果我想做一些事情,比如只查找每边被 50 个奇数包围的数字,那么编写这样的代码是完全没有意义的。

有没有一种通过函数式编程来解决这个问题的好方法,或者在这种情况下最好使用 for/while 循环样式?

CodePen 使用示例:https://codepen.io/jnpdx/pen/MvradM

【问题讨论】:

  • 我的第一个直觉是创建一个循环,也许递归会是一个有吸引力的解决方案。
  • 你不会写出所有这些条件。您只需 slice 数组并测试该切片的 every 元素上的条件。

标签: javascript functional-programming


【解决方案1】:

您可以对左右所需奇数的计数使用闭包,然后获取左右值并检查每个元素并返回检查结果以进行过滤。

一种特殊情况是计数为零,您只需检查实际元素。

var array = [1, 2, 3, 4, 5, 6, 8, 10, 11, 13, 15, 16, 17, 19],
    odd = item => item % 2,
    getOdds = count => (a, i, aa) => {
        var temp = aa.slice(i - count, i).concat(aa.slice(i + 1, i + 1 + count));
        return count
            ? temp.length === 2 * count && temp.every(odd)
            : odd(a);
    };

console.log(array.filter(getOdds(0)));
console.log(array.filter(getOdds(1)));
console.log(array.filter(getOdds(2)));
.as-console-wrapper { max-height: 100% !important; top: 0; }

更聪明的方法是计算连续的奇数部分并使用数组进行过滤。

检查16是否被两个奇数包围

name   values                                                     comment
-----  ---------------------------------------------------------  --------------------
array  [  1,  2,  3,  4,  5,  6,  8, 10, 11, 13, 15, 16, 17, 19]
left   [  1,  0,  1,  0,  1,  0,  0,  0,  1,  2,  3,  0,  1,  2]
right  [  1,  0,  1,  0,  1,  0,  0,  0,  3,  2,  1,  0,  2,  1]
                                                     16           item to check
                                                  3               left count >= 2
                                                          2       right count >= 2
                                                    true          result for filtering  

var array = [1, 2, 3, 4, 5, 6, 8, 10, 11, 13, 15, 16, 17, 19],
    odd = item => item % 2,
    left = array.reduce((r, a, i) => (r[i] = odd(a) ? (r[i - 1] || 0) + 1 : 0, r), []),
    right = array.reduceRight((r, a, i) => (r[i] = odd(a) ? (r[i + 1] || 0) + 1 : 0, r), []),
    getOdds = count => (a, i) => count
        ? left[i - 1] >= count && right[i + 1] >= count
        : odd(a);

console.log(array.filter(getOdds(0)));
console.log(array.filter(getOdds(1)));
console.log(array.filter(getOdds(2)));
.as-console-wrapper { max-height: 100% !important; top: 0; }

【讨论】:

  • count == 0 不是特例。 every 可以处理得很好。
  • @bergi,我不明白,请解释一下。
  • 只写return temp.every(odd)?而且我不明白为什么我们应该在没有环境的情况下检查元素本身。
  • 顺便说一句,我认为我们需要确保i - countslice 中不会变成负数
  • 没有。如果不检查长度和其他完整性检查,它不会以这种方式工作。
【解决方案2】:

函数式编程的整个想法是编写没有副作用的纯函数

Array.filter 是有效的,因为它返回一个新数组,而不会改变原始数组。您可以在同一个数组上运行该方法数百万次而无需更改它。

如果你的逻辑变得复杂,代码也变得复杂,没有功能魔法可以解决你的领域问题。

但是,您可以创建一个 createFilter 函数,该函数将根据您的域要求创建您的过滤器函数,例如:

const createFilter = ({
  before = e => true,
  after = e => true
}) => (entry, idx, entries) => 

之前(条目[idx - 1])&&之后(条目[idx + 1]) }; }

// This will return [ 4, 4 ] I guess ;)
[1, 3, 3, 3, 2, 4, 5, 2, 4, 7].filter(createFilter({
   before: (e) => e % 2 === 0,
   after: (e) => e % 2 === 1,
}))

同样的方法你可以只获取之前项目为 50 和之后项目为 100 的值:

[50, 1, 100, 4, 50, 3, 100].filter(createFilter({
  before: (e) => e === 50,
  after: (e) => e === 100
})) // pretty sure the output is [1, 3] 

这样你就有了一个可重复使用的filterCreator,根据你的需要扩展它;)

更新

@Aadit M Shah 是的,再次阅读 OP 后,我得出结论,我的方法仍然有效,您只需编写自己的 filterCreator 函数。 Array.filter 实际上并没有错。

const filterBySurrounding = (n, meetCondition) => {
  return (item, idx, array) => {
    return n <= idx && idx + n <= array.length - 1
      ? array.slice(idx - n, idx).every(meetCondition) &&
        array.slice(idx + 1, idx + 1 + n).every(meetCondition)
      : false

  }
}

const isOdd = n => n % 2 === 1
array.filter(filterBySurrounding(50, isOdd))

【讨论】:

  • 我认为您误解了这个问题。这不是 OP 想要的。
  • 感谢您指出,我已经创建了另一个示例,它解决了问题。
  • 您的filterBySurrounding 函数中有错误。应该是idx + n &lt;= array.length - 1。你忘记了最后的- 1
【解决方案3】:

这是我要做的:

const zipperFilter = (p, xs) => {
    const before = [];      // elements before x
    const after  = [...xs]; // x followed by elements after x, shallow copy
    const result = [];

    while (after.length > 0) {
        const x = after.shift(); // remove x; thus, after = elements after x
        if (p(before, x, after)) result.push(x);
        before.unshift(x); // before = x followed by elements before x
    }

    return result;
};

const isOdd = n => n % 2 === 1;

const surroundedByPossiblyNOddNumbers = n => (before, x, after) =>
    before.slice(0, n).every(isOdd) &&
    after.slice(0, n).every(isOdd);

const surroundedByStrictlyNOddNumbers = n => (before, x, after) =>
    before.length >= n &&
    after.length >= n &&
    before.slice(0, n).every(isOdd) &&
    after.slice(0, n).every(isOdd);

const xs = [1,2,3,4,5,6,8,10,11,13,15,16,17,19];

const ys = zipperFilter(surroundedByPossiblyNOddNumbers(1), xs);
const zs = zipperFilter(surroundedByPossiblyNOddNumbers(2), xs);
const as = zipperFilter(surroundedByStrictlyNOddNumbers(1), xs);
const bs = zipperFilter(surroundedByStrictlyNOddNumbers(2), xs);

console.log(JSON.stringify(ys));
console.log(JSON.stringify(zs));
console.log(JSON.stringify(as));
console.log(JSON.stringify(bs));

什么是zipperFilter?这是一个基于zipper data structure 的上下文敏感列表过滤功能。任何时候您想要进行上下文相关的数据处理(例如图像处理),都可以考虑使用 zippers。

创建自定义zipperFilter 函数的优点是:

  1. 比使用原生的filter 方法效率更高。这是因为我们不必一直使用slice 来生成beforeafter 数组。我们保留两者的运行副本,并在每次迭代时更新它们。
  2. before 数组以相反的顺序维护。因此,较低的索引总是对应于较近的邻居。这让我们可以简单地slice 获得我们想要的最近邻居的数量。
  3. 它具有可读性、通用性,并告知读者过滤是上下文相关的。

希望对您有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-06-17
    • 1970-01-01
    • 2021-04-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多