使用具有 ramda.js 的高阶函数进行映射

Mapping using higher-order functions with ramda.js

我的代码中有一个模式不断重复出现,看起来应该很常见,但我终究无法弄清楚它的名称或是否有通用的处理方法:maping 使用一个函数,该函数接受一个参数,该函数本身是一个函数将 maped 元素作为参数的结果。

这是模式本身。我已经将我想要的函数命名为 mapply(映射应用),但这似乎是错误的名称:

const mapply = (outer, inner) => el => outer(inner(el))(el)

这实际上叫什么?我怎样才能在惯用的 Ramda 中实现它?这似乎是世界上必须有聪明人告诉我如何处理它的事情。

我的用例是做一些基本的准牛顿物理工作,对物体施加力。要计算一些力,您需要一些关于物体的信息——位置、质量、速度等。一个(非常)简化的例子:

const g = Vector.create(0, 1),
    gravity = ({ mass }) => Vector.multiply(mass)(g),
    applyForce = force => body => {
        const { mass } = body, 
            acceleration = Vector.divide(mass)(force)

        return R.merge(body, { acceleration })
    }

//...

const gravitated = R.map(mapply(applyForce, gravity))(bodies)

谁能告诉我:这是什么?你会如何 Ramda-fy 呢?我应该注意哪些陷阱、边缘情况和困难?有什么聪明的处理方法?

(我搜索了又搜索——所以,Ramda 的 GitHub 存储库,以及其他一些函数式编程资源。但也许我的 Google-fu 不在需要的地方。抱歉如果我忽略了一些明显的东西。谢谢!)

这是一篇作文。它特别是 compose(或 pipe,如果你倒退的话)。

在数学中(考虑单变量微积分),你会有像 fxf(x) 这样的语句,表示有一些函数 f,它变换 x,以及变换应在别处描述...

当你看到 (g º f)(x) 时,你就会发疯。 "G of F"(或许多其他描述)。

(g º f)(x) == g(f(x))

看着眼熟?

const compose = (g, f) => x => g(f(x));

当然,您可以通过将组合函数用作组合函数内部的操作来扩展此范例。

const tripleAddOneAndHalve = compose(halve, compose(add1, triple));
tripleAddOneAndHalve(3); // 5

对于这个的可变参数版本,您可以执行以下两项操作之一,具体取决于您是想更深入地了解函数组合,还是想理顺一点。

// easier for most people to follow
const compose = (...fs) => x =>
  fs.reduceRight((x, f) => f(x), x);

// bakes many a noodle
const compose = (...fs) => x =>
  fs.reduceRight((f, g) => x => g(f(x)));

但是现在,如果你拿咖喱或部分 map 之类的东西,例如:

const curry = (f, ...initialArgs) => (...additionalArgs) => {
  const arity = f.length;
  const args = [...initialArgs, ...additionalArgs];
  return args.length >= arity ? f(...args) : curry(f, ...args);
};

const map = curry((transform, functor) =>
  functor.map(transform));

const reduce = ((reducer, seed, reducible) =>
  reducible.reduce(reducer, seed));

const concat = (a, b) => a.concat(b);

const flatMap = curry((transform, arr) =>
  arr.map(transform).reduce(concat, []));

你可以做一些漂亮的事情:

const calculateCombinedAge = compose(
  reduce((total, age) => total + age, 0),
  map(employee => employee.age),
  flatMap(team => team.members));

const totalAge = calculateCombinedAge([{
  teamName: "A",
  members: [{ name: "Bob", age: 32 }, { name: "Sally", age: 20 }],
}, {
  teamName: "B",
  members: [{ name: "Doug", age: 35 }, { name: "Hannah", age: 41 }],
}]);  // 128

非常强大的东西。当然,所有这些在 Ramda 中也可用。

const mapply0 = (outer, inner) => el => outer(inner(el))(el);

const mapply1 = (outer, inner) => R.converge(
  R.uncurryN(2, outer),
  [
    inner,
    R.identity,
  ],
);

const mapply2 = R.useWith(
  R.converge,
  [
    R.uncurry(2),
    R.prepend(R.__, [R.identity]),
  ],
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.24.1/ramda.min.js"></script>

我还没有测试过,但它可能会起作用。

首先是你的函数。

第二个使用 converge 传递 'el' 通过内部函数,然后是身份函数,并将两者传递到外部的非科里化版本。

R.uncurryN(2, outer) 就像这样 outer(inner(el), el),这意味着 converge 可以提供参数。

第三个可能太远了,但无论如何它都很有趣,你调用 converge 时第一个参数是 outer 的未柯里化版本,第二个参数是包含 inner 和身份的数组,useWith 这样做完全删除了函数定义来自解决方案。

我不确定这是否是您要找的,但这是我找到的 3 种写法。

转述自对问题的评论:

mapply 实际上是 chain:

R.chain(f, g)(x); //=> f(g(x), x)

嗯,主要是。在这种情况下,请注意 x 必须是一个数组。

那么我的解决方案是:

const gravitated = R.map(
    R.chain(applyForce, R.compose(R.of, gravity))
)(bodies)

Ramda 的 chain 文档在这种情况下并不是很有帮助:它只是简单地阅读,"chain maps a function over a list and concatenates the results." (ramdajs.com/docs/#chain)

答案隐藏在第二个示例中,其中两个函数被传递给链并部分应用。直到在这里阅读这些答案后我才明白这一点。

(感谢 ftor, bergi, and Scott Sauyet。)