使参数可用于Ramda管道功能中的所有功能

时间:2019-08-28 09:56:46

标签: functional-programming ramda.js

我在带有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)();
}

有什么办法可以准备一个通用包装器,将请求和响应对象包装在路由器上以供这些功能使用?我想我会 还必须更改其签名。

2 个答案:

答案 0 :(得分:1)

我认为这里真正需要的是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)将接收{{1} }参数从外部提供,其结果作为z传递给z

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

因此,下面是一个替代方案,它模仿一个普通的类似Express的环境,并使用稍有不同的(req, res) => (...args) => ...版本,该版本不需要额外的调用,只需调用不带参数的第一个函数,然后顺序传递结果按预期传递给其他人。这意味着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)

最后,请注意,Ramda和Express可能永远无法发挥特别出色的性能,因为发送给Express的处理程序旨在修改其参数,而Ramda则设计为永不突变输入数据。话虽如此,对于这些要求,这似乎工作得很好。

更新:pipe

的说明

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

doublePipe

有两个可能的电话:

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

如果您不熟悉Hindley-Milner签名(例如上面的// 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 ) ),我会在long article上写一个Ramda wiki,以了解它们在Ramda中的用途。通过将(a, b) -> c -> d-foo传递到f1来构建f4函数。结果函数采用类型doublePipea(在您的示例中为breq)的参数,并返回类型res的值。同样,通过向f提供bar-g1,并返回接受类型为g3doublePipe和{ {1}},并返回类型为a的值。

我们可以更强制性地重写b来显示所采取的步骤:

c

并将其扩展一点,这看起来也可能

f

在第一行中,我们将初始参数部分地应用于每个提供的函数,从而为我们提供了一些简单函数的列表。对于doublePipe,resultFns看起来像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这些函数并调用结果函数([f1(req, res), f2(req, res), f3(req, res), f4(req, res)]),但是我没有看到创建管道函数的充分理由,该函数只能调用一次并扔掉,所以我[Void -> c, c -> d, d -> e, e -> f]在该列表上,从pipe开始,并将每个结果传递到下一个。

我倾向于就Ramda函数进行思考,但是如果没有它们,您可以很容易地编写此代码:

return pipe(...resultFns)()

我希望这可以使事情变得更清楚!

答案 1 :(得分:0)

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

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

请注意,参数的顺序不是“随机”的。您需要处理的数据应该是最后一个,因为它可以提供更好的功能组合体验。这是Ramda的宗旨之一:

  

Ramda函数的参数进行了排列,以方便进行计算。通常要最后处理要处理的数据。

来源:https://ramdajs.com/

但这还不够。您需要咖喱一些。为什么?尽管功能组成的“配方”看起来相同,但是每个单独的功能都在特定数据上运行。稍后将有道理,让我们先咖喱:

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

重要的是要注意,尽管这没有什么“错”,但它也没有抓住重点:

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

为什么? R.pipeR.compose(或R.o)返回一个函数组合,该函数组合本身就是您使用参数调用的函数(仅使用R.o进行调用,但现在暂时忽略它)。因此,您需要考虑贯穿功能组合的数据。在您的情况下,可能是req

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

函数组合中的每个函数都将前一个函数的结果作为参数接收。如果介于两者之间的某些东西不依赖于此,那么也许该功能不应该是组成部分。 (请以少量盐作为建议;可能会适用特殊条件 ;只需考虑一下;)