首页 > 解决方案 > 使参数可用于 Ramda 管道函数中的所有函数

问题描述

Ramda在 node 中使用 express。我有一条标准路线:

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)();
}

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

标签: functional-programmingramda.js

解决方案


我认为这里真正需要的是一个版本,pipe它接受一些初始参数并返回一个接受剩余参数的新函数,所有函数都具有这样的双重应用程序签名。我想出了以下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从外部提供的参数,其结果传递zbar (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上面的),我在Ramda wiki上写了一篇关于它们在 Ramda 中的使用的长篇文章。该函数是通过传递-来构建的。生成的函数采用类型和(在您的示例中) 的参数并返回 type 的值。类似地,通过提供 -to 、返回一个接受类型参数的函数、和返回一个类型的值来创建。foof1f4doublePipeabreqresfbarg1g3doublePipeabcf

我们可以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)
}

在第一行中,我们将初始参数部分应用于每个提供的函数,为我们提供了一个更简单函数的列表。对于fooresultFns 看起来像[f1(req, res), f2(req, res), f3(req, res), f4(req, res)],它会有签名[Void -> c, c -> d, d -> e, e -> f]。我们现在可以选择pipe这些函数并调用生成的return pipe(...resultFns)()函数reduceundefined将每个结果传递给下一个。

我倾向于从 Ramda 函数的角度来思考,但是没有它们你可以很容易地写出来:

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

我希望这更清楚!


推荐阅读