5

Here are four functions I am trying to compose into a single endpoint string:

const endpoint = str => `${str}` || 'default'
const protocol = str => `https://${str}`
const params = str => `${str}?sort=desc&part=true&`
const query = str => `${str}query={ some:'value', another:'value'}`

let finalEndpoint = R.compose(query, params, protocol, endpoint)

var result = finalEndpoint('api.content.io')

This composition works and returns the result I want which is:

https://api.content.io?sort=desc&part=true&query={ some:'value', another:'value'}

But notice how I have hard coded the values for params and query inside their function body. I see only one value going up the value in this R.compose chain.

How and where exactly do I pass in parameters to the params and query parameters?

UPDATE:

What I did was curried those functions like this:

var R = require('ramda');

const endpoint = str => `${str}` || 'default'
const protocol = str => `https://${str}`
const setParams = R.curry ( (str, params) => `${str}?${params}` )
const setQuery = R.curry ( (str, query) => `${str}&query=${JSON.stringify(query)}` )

and then

let finalEndpoint = R.compose(protocol, endpoint)

var result = setQuery(setParams(finalEndpoint('api.content.io'), 'sort=desc&part=true'), { some:'value', another:'value'})

console.log(result);

But the final call to get result still seems pretty hacked and inelegant. Is there any way to improve this?

Chris Snow
  • 23,813
  • 35
  • 144
  • 309
Amit Erandole
  • 11,995
  • 23
  • 65
  • 103

3 Answers3

6

How and where exactly do I pass in parameters to the params and query parameters?

Honestly, you don't, not when you're building a compose or pipe pipeline with Ramda or similar libraries.

Ramda (disclaimer: I'm one of the authors) allows the first function to receive multiple arguments -- some other libraries do, some don't -- but subsequent ones will only receive the result of the previous calls. There is one function in Sanctuary, meld, which might be helpful with this, but it does have a fairly complex API.

However, I don't really understand why you are building this function in this manner in the first place. Are those intermediate functions actually reusable, or are you building them on spec? The reason I ask is that this seems a more sensible version of the same idea:

const finalEndpoint = useWith( 
  (endpoint, params, query) =>`https://${endpoint}?${params}&query=${query}`, [
    endpoint => endpoint || 'default', 
    pipe(toPairs, map(join('=')), join('&')), 
    pipe(JSON.stringify, encodeURIComponent)
  ]
);

finalEndpoint(
  'api.content.io', 
  {sort: 'desc', part: true},
  {some:'value', another:'value'}
);
//=> "https://api.content.io?sort=desc&part=true&query=%7B%22some%22%3A%22value%22%2C%22another%22%3A%22value%22%7D"

I don't really know your requirements for that last parameter. It looked strange to me without that encodeUriComponent, but perhaps you don't need it. And I also took liberties with the second parameter, assuming that you would prefer actual data in the API to a string encapsulating that data. But if you want to pass 'sort=desc&part=true', then replace pipe(toPairs, map(join('=')), join('&')) with identity.

Since the whole thing is far from points-free, I did not use a points-free version of the first function, perhaps or(__, 'default'), as I think what's there is more readable.

Update

You can see a version of this on the Ramda REPL, one that adds some console.log statements with tap.


This does raise an interesting question for Ramda. If those intermediate functions really are desirable, Ramda offers no way to combine them. Obviously Ramda could offer something like meld, but is there a middle ground? I'm wondering if there is a useful function (curried, of course) that we should include that works something like

someFunc([f0], [a0]); //=> f0(a0)
someFunc([f0, f1], [a0, a1]); //=> f1(f0(a0), a1)
someFunc([f0, f1, f2], [a0, a1, a2]); //=> f2(f1(f0(a0), a1), a2)
someFunc([f0, f1, f2, f3], [a0, a1, a2, a3]); //=> f3(f2(f1(f0(a0), a1), a2), a3)
// ...

There are some serious objections: What if the lists are of different lengths? Why is the initial call unary, and should we fix that by adding a separate accumulator parameter to the function? Nonetheless, this is an intriguing function, and I will probably raise it for discussion on the Ramda boards.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • Scott, I am only trying to learn and understand functional programming with Ramda. This is not a real use case. Of course the problem is trivial. I am trying to understand when to apply what part of the Ramdas solution and even if it possible or desirable to do so – Amit Erandole Jan 03 '17 at 00:39
  • This solution with `useWith` looks great but how do I account for missing parameters? `query` and `params` are both optional – Amit Erandole Jan 03 '17 at 01:41
  • also this is hard to debug - what do I do when I want to output the string after passing it to `stringify` and `encodeURIComponent` before I pass it to the rest of the functions? – Amit Erandole Jan 03 '17 at 01:47
  • 1
    Optional parameters, I'm afraid, don't mix well with Ramda. I can't think of any good way to combine them with `useWith`. – Scott Sauyet Jan 03 '17 at 02:00
  • 1
    Regarding debugging, you can always use `tap`. For instance `const log = bind(console.log, console)`. Then at the end of each of those pipelines, you can add `tap(log)`. I'll update the answer with a REPL link that demonstrates this. – Scott Sauyet Jan 03 '17 at 02:01
3

I wrote a little helper function for situations like this.

It is like compose, but with the rest params also passed in. The first param is the return value of the previous function. The rest params remain unchanged.

With it, you could rewrite your code as follows:

const compound = require('compound-util')

const endpoint = str => `${str}` || 'default'
const protocol = str => `https://${str}`
const params = (str, { params }) => `${str}?${params}`
const query = (str, { query }) => `${str}query=${query}`

const finalEndpoint = compound(query, params, protocol, endpoint)

const result = finalEndpoint('api.content.io', {
  params: 'sort=desc&part=true&',
  query: JSON.stringify({ some:'value', another:'value'})
})
pdme
  • 131
  • 2
1

If you have params and query as curried functions then you can:

EDIT: code with all the bells and whistles, needed to change parameter order or use R.__ and stringify object

const endpoint = R.curry( str => `${str}` || 'default' )
const protocol = R.curry( str => `https://${str}` )
const params = R.curry( (p, str) => `${str}?${p}` )
const query = R.curry( (q, str) => `${str}&query=${q}` )

let finalEndpoint =
    R.compose(
        query(JSON.stringify({ some:'value', another:'value' })),
        params('sort=desc&part=true'),
        protocol,
        endpoint
    )
var result = finalEndpoint('api.content.io')
console.log(result)
MadNat
  • 349
  • 2
  • 7
  • That doesn't work. It returns this absurdity: `[object Object]&query="sort=desc&part=true?https://api.content.io"` But can you show me a working version? – Amit Erandole Dec 31 '16 at 11:04
  • this updated answer is pretty close. One thing bothers me - how to programatically output `&` and other separators in the final string and still keep the functions pure? (because the separators between these segements are optional based on what is passed). Is there a way to delegate the chaining responsibility to a higher order function? – Amit Erandole Jan 03 '17 at 02:04
  • 1
    2 solutions come to my mind: 1. Pass `R.identity` in place of function you want to skip. 2. Make your functions of type: `String -> Maybe String -> String` that would check if parameter is `Nothing` or `Just params` and modify url accordingly – MadNat Jan 03 '17 at 13:02