在 compose 函数 Ramda 中访问数据

Accessing data inside compose function Ramda

我正在使用 Ramda 为一些数据提供结构。但是,我一直无法访问 compose.

中的数据
  1. 它应该映射 level 大于 2 的项目,但这不起作用

[keys, compose(map(map(find(propEq('level', > 2)))), values)]

  1. 我试图将所有项目保存在 typeChild 中作为 unique

这是用于测试的 ramda 控制台(必须跟随 link 通过那里,所以不允许 goo.gl links):http://dpaste.com/0SATTZK

const result = pipe(
  pluck('type'),
  groupBy(
    pipe(
      find(propEq('level', 1)),
      propOr('NoLevel', 'name'),
    )
  ),
  converge(
    zipWith(unapply(zipObj(['name', 'typeChild']))),
    [keys, compose(map(map(find(propEq('level', 2)))), values)]
  ),
);
result(data)

输入数据

[{
    "title": "Apple",
    "type": [{"name": "Food", "level": 1}, {"name": "Fruit", "level": 2}]
  }, {
    "title": "Tomato",
    "type": [{"name": "Food", "level": 1}, {"name": "Fruit", "level": 2}]
  }, {
    "title": "Potato",
    "type": [{"name": "Food", "level": 1}, {"name": "Vegetable", "level": 2}]
  }, {
    "title": "The Alchemist",
    "type": [{"name": "Entertainment", "level": 1}, { "name": "Book", "level": 2}]
  }, {
    "title": "Superman",
    "type": [{"name": "Entertainment", "level": 1}, {"name": "Movie", "level": 2}]
  }, {
    "title": "More facts",
    "type": [{"name": "Foo", "level": 2}]
  }, {
    "title": "Superman",
    "type": [{"name": "Bar", "level": 1}]
  }
];

期望的输出

[

{name: "Food", typechild: [{level: 2, name: "Fruit"}, {level: 2, name: "Vegetable"}]},
 {name: "Entertainment", typechild: [{level: 2, name: "Book"}, {level: 2, name: "Movie"}]},
 {name: "NoName", typechild: [{level: 2, name: "Foo"}]},
 {name: "Bar", typechild: []}
]

好的,我来猜猜你在找什么。


显示数据

首先,您确实需要演示输入数据的某些部分。我将其简化为:

const data =[{
    "title": "Apple",
    "type": [{"name": "Food", "level": 1}, {"name": "Fruit", "level": 2}]
  }, {
    "title": "Tomato",
    "type": [{"name": "Food", "level": 1}, {"name": "Fruit", "level": 2}]
  }, {
    "title": "Potato",
    "type": [{"name": "Food", "level": 1}, {"name": "Vegetable", "level": 2}]
  }, {
    "title": "The Alchemist",
    "type": [{"name": "Entertainment", "level": 1}, { "name": "Book", "level": 2}]
  }, {
    "title": "Superman",
    "type": [{"name": "Entertainment", "level": 1}, {"name": "Movie", "level": 2}]
  }, {
    "title": "More facts",
    "type": [{"name": "Foo", "level": 2}]
  }, {
    "title": "Superman",
    "type": [{"name": "Bar", "level": 1}]
  }
];

(请注意,我从每种类型中删除了 color 属性,因为它们似乎与讨论无关,但它们不会改变任何东西。)

而且我从您的尝试中猜测需要这样的输出:

[
 {name: "Food", typechild: [{level: 2, name: "Fruit"}, {level: 2, name: "Vegetable"}]},
 {name: "Entertainment", typechild: [{level: 2, name: "Book"}, {level: 2, name: "Movie"}]},
 {name: "NoName", typechild: [{level: 2, name: "Foo"}]},
 {name: "Bar", typechild: []}
]

通过组合函数来解决这个问题

这是一种方法:

const levelEq = (n) => pipe(prop('level'), equals(n));
const topLevel = pipe(prop('type'), find(levelEq(1)));
const topLevelName = pipe(topLevel, propOr('NoName', 'name'));
const extract2ndLevel = pipe(pluck('type'), flatten, filter(levelEq(2)));

const convert = pipe(
  groupBy(topLevelName),
  map(extract2ndLevel),
  map(uniq),
  toPairs,
  map(zipObj(['name', 'typechild']))
);

convert(data); //=> (the first output format above)

(通常对于那些单行,而不是 pipe,我会使用 compose,并颠倒顺序,但我也不太喜欢混合使用 composepipe 在同一个脚本中。对于更长的 convert 函数,我绝对更喜欢 pipe。但是切换其中任何一个,或者将它们组合起来都不会改变任何重要的东西。)

重点是,这是建立在功能组合之上的。我没有尝试一次构建它,而是编写单独的函数来完成小工作并将它们组合成更复杂的工作。

请注意,此代码不会优雅地处理错误数据,更改为这样做可能会很麻烦。

另请注意,在 main 函数中,我一次处理一小步。我可以注释掉后续步骤以查看每个步骤的结果。如果我愿意,我也可以使用 R.tap

重新组合很少是个好主意

除了相对简单的 levelEq 之外,每个辅助函数都只使用一次。所以他们可以很容易地内联。我们可以这样重写这段代码:

const convert = pipe(
  groupBy(pipe(prop('type'), find(pipe(prop('level'), equals(1))), propOr('NoName', 'name'))),
  map(pipe(pluck('type'), flatten, filter(pipe(prop('level'), gte(__, 2))), uniq)),
  toPairs,
  map(zipObj(['name', 'typechild']))
);

但对我来说这是一团乱麻,我不会打扰。

更好的文档

如果您习惯了 Hindley-Milnar style type annotation,为这些函数添加类型签名可能会有所帮助,例如:

// Type :: {name: String, level: Int}

// :: Int -> (Type -> Bool)
const levelEq = (n) => pipe(prop('level'), equals(n));
// :: {type: [Type]} -> Type
const topLevel = pipe(prop('type'), find(levelEq(1)));
// :: {type: [Type]} -> String
const topLevelName = pipe(topLevel, propOr('NoName', 'name'));
// :: [{title: String, type: [Type}]}] -> [Type]
const extract2ndLevel = pipe(pluck('type'), flatten, filter(levelEq(2)));

// [{title: String, type: [Type]}] -> [{name: String, typechild: [Type]}]
const convert = pipe( /* ... */ )

(如果这些对您来说毫无意义,请不要担心。)

更改输出格式

但也许你真的想要这样的东西:

[
 {"name": "Food", "typechild": ["Fruit", "Vegetable"]}, 
 {"name": "Entertainment", "typechild": ["Book", "Movie"]}, 
 {"name": "NoName", "typechild": ["Foo"]}, 
 {"name": "Bar", "typechild": []}
]

事实证明这是一个简单的更改:

const convert = pipe(
  groupBy(topLevelName),
  map(extract2ndLevel),
  map(uniq),
  map(pluck('name')), // <--- A single addition
  toPairs,
  map(zipObj(['name', 'typechild']))
);

map

的优势

我们在最后一个片段中看到的一件事是一系列连续的 map 调用。其中每一个都分别循环遍历列表。这有助于编写干净的代码,但是如果在性能测试中,您发现这个额外的循环导致了您的问题,您可以利用 composition law associated with map,它被适当地翻译为

pipe(map(f), map(g)) ≍ map(pipe(f, g))

所以你可以添加这个:

// :: [{title: String, type: [Type}]}] -> [String]
const foo = pipe(extract2ndLevel, uniq, pluck('name'));

然后像这样重写主函数:

// [{title: String, type: [Type]}] -> [{name: String, typechild: [Type]}]
const convert = pipe(
  groupBy(topLevelName),
  map(foo),
  toPairs,
  map(zipObj(['name', 'typechild']))
);

但是我想不出这个新函数的好名字这一事实让我觉得它不是一个很好的抽象;如果实际性能测试表明多次迭代是一个现实世界的问题,我只会选择这样做。

结论

函数式编程涉及很多方面,但关键技术之一是不断地将所有内容分解为易于理解的部分。这就是我尝试使用此解决方案所做的。虽然我们可以打破它来创建没有依赖关系的单个函数("Recombining..." 以上 ),但很少可读。另一方面,这种方法可以很容易地改变我们的方法 ("Change Output formats"),并在必要时解决性能问题 ("Advantages of map").

哇,那应该是一个博客 post!


您可以在 Ramda REPL.

上看到其中的大部分内容。