使 Ramda 管道函数中的所有函数都可以使用参数

Make parameters available to all functions inside Ramda's pipe function

我在带有 express 的节点中使用 Ramda。我有一条标准路线:

app.get('/api/v1/tours', (req, res) => {

}

我想使用 Ramda 组合函数,但我将这些函数写在路由之外(因此它们可以在其他路由中重用)。 例如:

function extractParams() {
  return req.query.id;
}

function findXById(id) {
  return xs.find(el => el.id == id);
}

function success(answer) {
  return res.status(200).json(answer);
}

现在我想在几个路由器中组合这些函数。其中之一是:

app.get('/api/v1/tours', (req, res) => {
  return R.pipe(extractParams, findXById, success)();
}

有什么方法可以准备一个通用包装器来包装路由器上的请求和响应对象以供这些功能使用?我想我会 也必须更改他们的签名。

你的三个函数在声明的范围内没有他们需要的东西。您需要先修改他们的签名:

function extractParams(req) { //<-- added `req`
  return req.query.id;
}

function findXById(id, xs) { //<-- added `xs`
  return xs.find(el => el.id == id);
}

function success(res, answer) { //<-- added `res`
  return res.status(200).json(answer);
}

请注意,参数的顺序不是 "random"。您需要操作的数据应该放在最后,因为它可以提供更好的函数组合体验。这是 Ramda 的宗旨之一:

The parameters to Ramda functions are arranged to make it convenient for currying. The data to be operated on is generally supplied last.

来源:https://ramdajs.com/

但这还不够。你需要咖喱其中一些。为什么?虽然函数组合的 "recipe" 看起来相同,但每个单独的函数都对特定数据进行操作。这个后面说得通,先咖喱一下:

const extractParams = (req) => req.query.id;
const findXById = R.curry((id, xs) => xs.find(el => el.id == id));
const success = R.curry((res, answer) => res.status(200).json(answer));

现在您可以构建函数组合,同时为组合中的函数提供一些特定参数:

app.get('/api/v1/tours', (req, res) =>
  R.pipe(
    extractParams,
    findXById(42),
    success(res))
      (req));

重要的是要注意,虽然这没有什么 "wrong",但它也遗漏了要点:

R.pipe(extractParams, findXById, success)()

为什么? R.pipe or R.compose (or R.o) returns 一个函数组合,它本身就是一个你用参数调用的函数(只有一个带有 R.o 但让我们忽略它目前)。因此,您需要考虑通过函数组合的数据。在你的情况下,它可能是 req:

R.pipe(extractParams, findXById, success)(req)

函数组合中的每个函数都接收前一个函数的结果作为其参数。如果介于两者之间的某些东西不依赖于此,那么 或许 该函数不应成为组合的一部分。 (对这个建议持保留态度;特殊条件 可能 适用;请考虑一下 ;)

我认为这里真正需要的是 pipe 的一个版本,它接受一些初始参数和 returns 一个接受剩余参数的新函数,所有函数都具有这样的双重 -申请签名。我提出了以下 doublePipe 执行此操作的实现:

const doublePipe = (...fns) => (...initialArgs) => 
  pipe (...(map (pipe (apply, applyTo (initialArgs)), fns) ))


const foo = (x, y) => (z) => (x + y) * z
const bar = (x, y) => (z) => (x + y) * (z + 1)

const baz = doublePipe (foo, bar)

console .log (
  baz (2, 4) (1) //=> (2 + 4) * (((2 + 4) * 1) + 1) => 42
  //                   /    \    '------+----'
  //        bar ( x --/  ,   `-- y    ,  `-- z, which is foo (2, 4) (1) )
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, map, apply, applyTo} = R                </script>

请注意,函数 foobar 都将接收相同的 xy 参数,并且 foo (x, y) 将接收 z 参数从外部提供,其结果作为 z 传递给 bar (x, y).

这是一个有趣的函数,它是解决这类问题的一个相当有用的通用解决方案。 但是 它在您的 Express 环境中不起作用,因为处理程序需要具有签名 (req, res) => ... 而不是 (req, res) => (...args) => ....

所以下面是一个替代方案,它模仿一个普通的类似 Express 的环境并使用稍微不同的 doublePipe 版本,它不需要额外的调用,只需调用第一个没有参数的函数,然后按预期顺序将结果传递给其他人。这意味着 doublePipe 的第一个函数必须具有签名 (req, res) => () => ...,而其他函数具有 (req, res) => (val) => ...。虽然我们可以修复它,使第一个只是 (req, res) => ...,但在我看来,这种不一致不会有帮助。

const doublePipe = (...fns) => (...initialArgs) => 
  reduce (applyTo, void 0, map (apply (__, initialArgs), fns))


const xs = [{id: 1, val: 'abc'}, {id: 2, val: 'def'},{id: 3, val: 'ghi'}, {id: 4, val: 'jkl'}]

const extractParams = (req, res) => () => req .query .id
const findXById = (xs) => (req, res) => (id) => xs .find (el => el .id == id)
const success = (req, res) => (answer) => res .status (200) .json (answer)

app .get ('/api/v1/tours', doublePipe (extractParams, findXById (xs), success))

console .log (
  app .invoke ('get', '/api/v1/tours?foo=bar&id=3')
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>

const {__, map, reduce, applyTo, apply, head, compose, split, objOf, fromPairs, last} = R

// Minimal version of Express, only enough for this demo

const base = compose (head, split ('?'))
const makeRequest = compose (objOf ('query'), fromPairs, map (split ('=')), split ('&'), last, split ('?'))
const makeResponse = () => {
  const response = {
    status: (val) => {response .status = val; return response},
    json: (val) => {response.body = JSON .stringify (val); delete response.json; return response}
  }
  return response
}
const app = {
  handlers: {get: {}, post: {}},
  get: (route, handler) => app .handlers .get [route] = handler,
  invoke: (method, route) => 
    app .handlers [method] [base (route)] (makeRequest (route), makeResponse ())
}


</script>

findById 没有所需的签名,但 findById(xs) 有,所以这就是我们传递给 pipe.

的内容

最后,请注意 Ramda 和 Express 可能永远不会一起玩得特别好,因为发送到 Express 的处理程序旨在修改它们的参数,而 Ramda 被设计为永远不会改变输入数据。也就是说,这似乎可以很好地满足这些要求。

更新:doublePipe

的解释

一条评论似乎表明需要对 doublePipe 进行更完整的描述。我只讨论第二个版本,

const doublePipe = (...fns) => (...initialArgs) => 
  reduce (applyTo, void 0, map (apply (__, initialArgs), fns))

这里有两个可能的调用:

// foo :: (a, b) -> f
const foo = doublePipe (
  f1, // :: (a, b) -> Void -> (c)
  f2, // :: (a, b) -> c -> d
  f3, // :: (a, b) -> d -> e
  f4, // :: (a, b) -> e -> f
)     

// bar :: (a, b, c) -> f
const bar = doublePipe (
  g1, // :: (a, b, c) -> Void -> d
  g2, // :: (a, b, c) -> d -> e
  g3, // :: (a, b, c) -> e -> f    
)

如果您不熟悉 Hindley-Milner 签名(例如上面的 (a, b) -> c -> d),我写了一个 long article on the Ramda wiki 关于它们在 Ramda 中的用途。 foo 函数是通过将 f1 - f4 传递给 doublePipe 来构建的。生成的函数采用 ab 类型的参数(在您的示例中为 reqres)和 returns 类型 f 的值。类似地,bar 是通过向 doublePipe 提供 g1 - g3 创建的,返回一个函数,该函数接受类型为 abc 并返回 f.

类型的值

我们可以更命令地重写 doublePipe 以显示所采取的步骤:

const doublePipe = (...fns) => (...initialArgs) => {
  const resultFns = map (apply (__, initialArgs), fns)
  return reduce (applyTo, void 0, resultFns)
}

稍微扩展一下,这可能也像

const doublePipe = (...fns) => (...initialArgs) => {
  const resultFns = map (fn => fn(...initialArgs), fns)
  return reduce ((value, fn) => fn (value), undefined, resultFns)
}

在第一行中,我们将初始参数部分地应用于每个提供的函数,为我们提供了一个更简单的函数列表。对于 foo,resultFns 看起来像 [f1(req, res), f2(req, res), f3(req, res), f4(req, res)],它的签名是 [Void -> c, c -> d, d -> e, e -> f]。我们现在可以选择 pipe 那些函数并调用结果函数 (return pipe(...resultFns)()),但我没有看到创建管道函数只是为了调用它一次并将其丢弃的充分理由,所以我 reduce 遍历该列表,从 undefined 开始并将每个结果传递给下一个。

我倾向于根据 Ramda 函数来思考,但是没有它们你也可以很容易地写出这个:

const doublePipe = (...fns) => (...initialArgs) => 
  fns 
    .map (fn => fn (...initialArgs))
    .reduce ((value, fn) => fn (value), void 0)

我希望这更清楚!