有没有更优雅的方法来解决这个 Ramda 过滤器问题?

Is there a more elegant way to solve this Ramda filter problem?

var a = { id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' };
var b = { id: 2, name: 'name2', type: 'type1', tag: 'tag2' };
var c = { id: 3, name: 'name3', type: 'type1', tag: 'tag1' };
var d = { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' };
var e = { id: 5, name: 'name4', type: 'type2', tag: 'tag4' };
var f = { id: 6, name: 'name5', type: 'type2', tag: 'tag3' };

var arr = [a, b, c, d, e, f];

var typeFind = R.curry((type, ex) => R.find(ele => ele.type === type, ex));
var tagEqProps = R.curry((o1, o2) => R.eqProps('tag', o1, o2));
var filtered = R.curry((arr, ex) => R.filter(ele => tagEqProps(ele, typeFind(ele.type, ex)), arr));

var excludes = R.filter(ele => ele.name === 'filter-name', arr);
filtered(arr, excludes);

我需要过滤:

  1. 名称为 'filter-name'
  2. 的对象
  3. 属性 'tag' 等于 'filter-name' 对象的每个类型

在这种情况下,结果是 1、3、4、6。

有没有更优雅的方法解决这个问题?

使用 R.filter with R.propEq to get all items with the a name that is equal to filter-name, and pluck the tags. Then use R.innerJoin,使用您选择的标签列表来获取所有带有相关标签的项目:

const { pipe, filter, propEq, innerJoin, pluck } = R;

const getFiltered = pipe(
  filter(propEq('name', 'filter-name')),
  pluck('tag')
);

const getByTags = arr => innerJoin(
  (record, tag) => record.tag === tag,
  arr,
  getFiltered(arr)
);

const arr = [{"id":1,"name":"filter-name","type":"type1","tag":"tag1"},{"id":3,"name":"name3","type":"type1","tag":"tag1"},{"id":4,"name":"filter-name","type":"type2","tag":"tag3"},{"id":6,"name":"name5","type":"type2","tag":"tag3"}];

const result = getByTags(arr);

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

下面是一系列版本,展示了如何在一些自定义组合器的帮助下制作此函数的无点版本。不过,最后,我认为这完全不值得,另一个带有中间变量的版本——使用或不使用 Ramda 编写——可读性要高得多。

第一关

很明显,这个问题有两个不同的步骤:首先,找到与 filter-name 匹配的标签,其次,找到具有这些标签名称的元素。让我们为这些编写函数:

const tagNames = (filterName) => (arr) => 
  pipe (filter (propEq ('name', filterName)), pluck ('tag')) (arr)

const getItems= (propNames) => (arr) => filter (anyPass (map (propEq ('tag'), propNames))) (arr)

const matchTags = (filterName) => (arr) => 
  getItems (tagNames (filterName) (arr)) (arr)

const arr = [{ id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' }, { id: 2, name: 'name2', type: 'type1', tag: 'tag2' }, { id: 3, name: 'name3', type: 'type1', tag: 'tag1' }, { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' }, { id: 5, name: 'name4', type: 'type2', tag: 'tag4' }, { id: 6, name: 'name5', type: 'type2', tag: 'tag3' }]

console .log (matchTags ('filter-name') (arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, filter, propEq, pluck, anyPass, map} = R        </script>

这些是相关 Ramda functions 的直接用法。我们通过比较简单的方式将这两个函数组合成matchTags。在两个不同的点通过 arr 有点令人惊讶,但考虑到要求,这不应该太令人惊讶。

迈向无积分

当最终柯里化参数仅用作对底层函数的调用并返回该结果时,我们可以简单地删除参数和该调用并获得等效函数。换句话说,对于任何一元函数 foo(arg) => foo(arg) 等价于 foo。对于带有参数 arrtagNamesgetItems 都是这种情况。所以我们可以像这样简化上面的内容:

const tagNames = (filterName) => 
  pipe (filter (propEq ('name', filterName)), pluck ('tag'))

const getItems = (propNames) => filter (anyPass (map (propEq ('tag'), propNames)))

然后我们可以更进一步,从 getItems 中删除 propNames:

const getItems = pipe (map (propEq ('tag')), anyPass, filter)

您可以在以下代码段中验证我们没有破坏任何东西:

const tagNames = (filterName) => 
  pipe (filter (propEq ('name', filterName)), pluck ('tag'))

const getItems = pipe (map (propEq ('tag')), anyPass, filter)

const matchTags = (filterName) => (arr) => 
  getItems (tagNames (filterName) (arr)) (arr)

const arr = [{ id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' }, { id: 2, name: 'name2', type: 'type1', tag: 'tag2' }, { id: 3, name: 'name3', type: 'type1', tag: 'tag1' }, { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' }, { id: 5, name: 'name4', type: 'type2', tag: 'tag4' }, { id: 6, name: 'name5', type: 'type2', tag: 'tag3' }]

console .log (matchTags ('filter-name') (arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, filter, propEq, pluck, anyPass, map} = R        </script>

添加组合器

我们一直在寻找一种使 matchTagstagNames 无积分的简单方法。他们多次使用他们的论点,或者在最后的位置以外的位置使用他们的论点。我们当然可以这样保留它,但我们可能会注意到其中有些模式我们可能想要重用。一种方法是使用 Combinators。组合器将简单的函数组合成更复杂的函数,许多重要的 Ramda 函数充当组合器,包括该列表中提到的所有 Ramda 函数和其他函数,例如 convergeuseWith.

让我们看看matchTags的结构,给我们看到的部分命名:

const matchTags = (filterName) => (arr) => 
//                 `----x----'    `-y-' 
  getItems (tagNames (filterName) (arr)) (arr)
//`---f--'  `---g--' `-----x----' `-y-'  `-y-'

我们可以编写一个非常简单的函数来捕获该结构,并用它来构建 matchTags 更简单的部分。有使用单个大写字母来命名此类组合器或使用某些鸟类名称的强大传统。我们不想重用其中任何一个,也不想为我们不确定通常有用的函数起任何真正的暗示性名称。所以现在,让我们称这个函数为 Z1。我们可以这样使用它:

const Z1 = (f) => (g) => (x) => (y) =>
  f (g (x) (y)) (y)

const matchTags = Z1 (getItems) (tagNames)

我们没有消除任何复杂性——我们只是将它分流了。我们让 Z1 处理函数组合的复杂性,并用它来简化 matchTags。这可能是值得的。但是,如果我们认为 Z1 是一个足够重要的函数,我们想要重用它并可能给它一个更具描述性的名称,那么它绝对是值得的。

我们可以通过稍微重写 tagNames:

来做同样的事情
const tagNames = (filterName) => (arr) =>
//                `----x---'     `-y-'
  pluck('tag') ((pipe (propEq ('name'), filter)) (filterName) (arr))
//`----f-----'  `--------------g--------------'   `----x---'  `-y-'

我们现在可以添加 Z2,像这样:

const Z2 = (f) => (g) => (x) => (y) =>
  f (g (x) (y))

const tagNames = Z2 (pluck('tag')) (pipe (propEq ('name'), filter))

Z2看起来比较眼熟。这很有可能是一个现有的命名组合器。如果我们选择,我们可以尝试找到可能与此相关联的名称。这留作 reader.

的练习

结合所有这些,并内联辅助函数,我们可以这样重写:

const matchTags = Z1
  (pipe (map (propEq ('tag')), anyPass, filter))
  (Z2 (pluck('tag')) (pipe (propEq ('name'), filter)))

它仍然有效,如以下代码片段所示:

const Z1 = (f) => (g) => (x) => (y) =>
  f (g (x) (y)) (y)

const Z2 = (f) => (g) => (x) => (y) =>
  f (g (x) (y))

const matchTags = Z1
  (pipe (map (propEq ('tag')), anyPass, filter))
  (Z2 (pluck('tag')) (pipe (propEq ('name'), filter)))

const arr = [{ id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' }, { id: 2, name: 'name2', type: 'type1', tag: 'tag2' }, { id: 3, name: 'name3', type: 'type1', tag: 'tag1' }, { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' }, { id: 5, name: 'name4', type: 'type2', tag: 'tag4' }, { id: 6, name: 'name5', type: 'type2', tag: 'tag3' }]

console .log (matchTags ('filter-name') (arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, filter, propEq, pluck, anyPass, map} = R        </script>

这值得吗?

我们已经创建了两个新的可能可重用的组合器,并且我们重构了我们的主要代码以使用它们,现在它是无点的。那么问题来了,值得吗?

我的答案? 没有!这是可怕的代码!

即使我们可能会发现这些组合子的其他用途,除非我们大量使用它们,否则我们不会记得 Z1Z2 的作用。也许我们会找到更具描述性的名称,这也许会有所帮助。但这很有可能永远是个谜。

如果需求如此神秘以至于任何代码都会很复杂,那么这可能就可以了。但我们可以做得更好。

更简单的实现

明确地编写所需的两个步骤,中间有一个局部变量使代码更易于阅读:

const matchTags = (filterName, arr) => {
  const tags = pluck ('tag', filter (propEq ('name', filterName), arr))
  return filter (propSatisfies (includes(__, tags), 'tag'), arr)
}

const arr = [{ id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' }, { id: 2, name: 'name2', type: 'type1', tag: 'tag2' }, { id: 3, name: 'name3', type: 'type1', tag: 'tag1' }, { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' }, { id: 5, name: 'name4', type: 'type2', tag: 'tag4' }, { id: 6, name: 'name5', type: 'type2', tag: 'tag3' }]


console .log (matchTags ('filter-name', arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {__, pluck, filter, propEq, propSatisfies, includes} = R  </script>

注意变量 tags。在示例情况下,它存储 ['tag1', 'tag3']。使用它使代码感觉更像我们的初始通道,其中功能按流程步骤分离。

对我来说,这是一个清晰易懂的实现。当然,这不是唯一的。而且我永远不会为了创建这样的函数而引入 Ramda,因为普通的 JS 版本同样干净:

const matchTags = (filterName, arr) => {
  const tags = arr
    .filter (({name}) => name == filterName)
    .map (({tag}) => tag)
  return arr .filter (({tag}) => tags .includes (tag))
}

在他们之间,我不确定我更喜欢 Ramda 版本还是 vanilla JS 版本,但在我看来,任何一个都可以。

课程

许多 Ramda 新用户似乎认为无积分本身就是一个重要目标。

相反,我建议它只是我们工具箱中的一个工具。只要它使我们的代码易于阅读和编写,我们就应该使用它。当它混淆事物时,我们应该完全避免它。

这是我必须反复学习的一课。上面的实现链非常好地解决了这个问题。尽管我宣扬这个 "point-free is not for everything" 想法,但我仍然发现自己经常陷入其中。