Reputation: 1077
I'm using Ramda
in node with express. I have a standard route:
app.get('/api/v1/tours', (req, res) => {
}
Where I'd like to compose functions using Ramda, but I write these functions outside the route (so they will be reusable in other routes). For example:
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);
}
Now I want to compose those functions inside several routers. One of them will be:
app.get('/api/v1/tours', (req, res) => {
return R.pipe(extractParams, findXById, success)();
}
Is there any way I can prepare a generic wrapper that wraps the request and response objects on the routers to be available to these functions? I guess I'll also have to change their signature.
Upvotes: 0
Views: 419
Reputation: 50807
I think what's really needed here is a version of pipe
that accepts some initial arguments and returns a new function that will accept the remaining ones, with all the functions having such a dual-application signature. I came up with the following doublePipe
implementation that does this:
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>
Note that the functions foo
and bar
will both receive the same x
and y
arguments, and that foo (x, y)
will receive the z
argument supplied from the outside, with its result passed as z
to bar (x, y)
.
This is an interesting function, and it's a fairly useful generic solution to this sort of problem. But it won't work in your Express environment, because the handlers need to have the signature (req, res) => ...
and not (req, res) => (...args) => ...
.
So below is an alternative, which mimics a trivial Express-like environment and uses a slightly different doublePipe
version, which does not take an additional invocation, simply calling the first function with no parameters, and then sequentially passing the results through to the others as expected. This means the first function to doublePipe
must have the signature (req, res) => () => ...
, while the others have (req, res) => (val) => ...
. While we could fix it so that that the first one was just (req, res) => ...
, it seems to me that this inconsistency would not be helpful.
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
does not have the required signature, but findById(xs)
does, so that's what we pass into pipe
.
Finally, note that Ramda and Express may never play particularly well together, as the handlers sent to Express are meant to modify their parameters, and Ramda is designed to never mutate input data. That said, this seems to work reasonably well for these requirements.
doublePipe
A comment seemed to indicate that a more complete description of doublePipe
was in order. I will only discuss the second version,
const doublePipe = (...fns) => (...initialArgs) =>
reduce (applyTo, void 0, map (apply (__, initialArgs), fns))
Here are two possible calls:
// 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
)
If you're not familiar with the Hindley-Milner signatures (such as (a, b) -> c -> d
above), I wrote a long article on the Ramda wiki about their uses in Ramda. The foo
function is built by passing f1
- f4
to doublePipe
. The resulting function takes parameters of types a
and b
(req
and res
in your example) and returns a value of type f
. Similarly bar
is created by supplying g1
- g3
to doublePipe
, returning a function that accepts parameters of types a
, b
, and c
and returning a value of type f
.
We can rewrite doublePipe
a bit more imperatively to show the steps taken:
const doublePipe = (...fns) => (...initialArgs) => {
const resultFns = map (apply (__, initialArgs), fns)
return reduce (applyTo, void 0, resultFns)
}
and expanding that a bit, this might also look like
const doublePipe = (...fns) => (...initialArgs) => {
const resultFns = map (fn => fn(...initialArgs), fns)
return reduce ((value, fn) => fn (value), undefined, resultFns)
}
In the first line, we partially apply the initial arguments to each of the supplied functions, giving us a list of simpler functions. For foo
resultFns would look like [f1(req, res), f2(req, res), f3(req, res), f4(req, res)]
, which would have signatures [Void -> c, c -> d, d -> e, e -> f]
. We could now choose to pipe
those functions and call the resulting function (return pipe(...resultFns)()
), but I didn't see a good reason to create the piped function only to call it a single time and throw it away, so I reduce
over that list, starting with undefined
and passing the result of each one to the next.
I tend to think in terms of Ramda functions, but you could write this easily enough without them:
const doublePipe = (...fns) => (...initialArgs) =>
fns
.map (fn => fn (...initialArgs))
.reduce ((value, fn) => fn (value), void 0)
I hope this made that clearer!
Upvotes: 1
Reputation: 18941
Your three functions do not have the things they need in their declared scope. You need to modify their signature first:
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);
}
Note that the order of the parameters isn't "random". The data you need to operate on should be the last as it allows for a nicer function composition experience. It's one of the tenet of Ramda:
The parameters to Ramda functions are arranged to make it convenient for currying. The data to be operated on is generally supplied last.
Source: https://ramdajs.com/
This is not enough though. You need to curry some of them. Why? While the "recipe" of your function composition looks the same, each individual function operate on a specific data. This will make sense later, let's curry first:
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));
Now you can build a function composition whilst supplying some specific parameter to your functions in the composition:
app.get('/api/v1/tours', (req, res) =>
R.pipe(
extractParams,
findXById(42),
success(res))
(req));
It's important to note that while there is nothing "wrong" with this, it's also missing the point:
R.pipe(extractParams, findXById, success)()
Why? R.pipe
or R.compose
(or R.o
) returns a function composition which is itself a function that you call with parameters (just one with R.o
but let's ignore that for now). So you need to think about the data that goes through your function composition. In your case it's probably req
:
R.pipe(extractParams, findXById, success)(req)
Each function in your function composition receives as its parameter, the result of the previous function. If something in between doesn't depend on that, then perhaps that function shouldn't be part of the composition. (Take that advice with a pinch of salt; special conditions may apply; just think about it ;)
Upvotes: 0