springloaded
springloaded

Reputation: 1079

How to curry a function that takes an options object as argument rather than several distinct arguments?

I have been looking into partial application and currying over the last few days.

I'm wondering how could I use these concepts with a function that only takes one options object as argument.

const myFunc = options => {
  const options.option1 = options.option1 || 'default value';
  const options.option2 = options.option2 || 'another default value';
  // ... etc, it takes about 5 more options, all of which have a
  // default fall-back value if not supplied

  return doSometing(options);
}

In that case, I don't feel good changing the myFunc signature and pass every option as a separate argument because it's a pain to remember the order in which the options must be supplied.

I'd like to stick with a functional style and avoid instantiating objects with new ... just to keep state; I have a hunch this can be achieved with partial application. It keeps things simpler when it's time for testing, or to instantiate.

But then, I don't know how to do partial application properly without separate arguments.

How would you handle this refactor?

Upvotes: 2

Views: 989

Answers (3)

user6445533
user6445533

Reputation:

I don't think your problem is connected with partial application. What exactly does myFunc do actually?

  • it sets a couple of optional default values
  • it invokes another function

This is not much. And yet two problems arise:

  • the function composition is hard coded and hidden in the body of myFunc
  • it doesn't get apparent from the function signature which default values are overwritten

Simply put, a "proper" function reveals its functionality by its signature. So let's get rid of the myFunc wrapper:

const options = {
  foo: 1,
  bar: true,
  bat: "",
  baz: []
};

// function composition

const comp = (...fs) => x => fs.reduceRight((acc, f) => f(acc), x);

// applicator for partial application with right section

const _$ = (f, y) => x => f(x) (y); // right section

// `Object` assignment

const assign = o => p => Object.assign({}, o, p);

// specific function of your domain

const doSomething = o => (console.log(o), o);

// and run (from right-to-left)

comp(doSomething, _$(assign, {baz: [1, 2, 3], bat: "abc"})) (options);

Now you can exactly see what is going on without having to look into the function bodies. The property order of the options Object doesn't matter either.

A remark on _$. It has this odd name because I prefer a visual name over a textual one in this particular case. Given the function sub = x => y => x - y, _$(sub, 2) simply means x => x - 2. This is called the right section of the partially applied sub function, because the "left" argument is still missing.

Upvotes: 1

Scott Sauyet
Scott Sauyet

Reputation: 50807

Following on Dan D's answer and the comments, this technique would let you partially apply such a function repeatedly until all the required fields are supplied:

const vals = (obj) => Object.keys(obj).map(key => obj[key]);

const addDefaults = (() => {
  const req = Symbol();
  const addDefaults = (defaults, fn) => (options) => {
    const params = Object.assign({}, defaults, options);
    return (vals(params).includes(req)) 
      ? addDefaults(params, fn)
      : fn(params);
  };
  addDefaults.REQUIRED = req;
  return addDefaults;
})();


const order = addDefaults({
  color: addDefaults.REQUIRED,
  size: addDefaults.REQUIRED,
  giftWrap: false,
  priority: false
}, (opts) => 
   `${opts.size}, ${opts.color}, ${opts.giftWrap ? '' : 'no'} wrap, priority: ${opts.priority}`
);

order({color: 'Red', size: 'Medium'}); // "Medium, Red, no wrap, priority: false"

const orderLarge = order({size: 'Large'}); // Options -> String
orderLarge({color: 'Blue'}); // "Large, Blue, no wrap, priority: false"

Upvotes: 2

Dan D.
Dan D.

Reputation: 74655

I would suggest that the equivalent of currying a function taking an option object would be the same as how to handle defaults. Consider this as a partial applier:

myFuncWithMyOptions(myFunc, myOptions) {
  return function(options) {
    return myFunc(Object.assign({}, myOptions, options));
  }
}

If you want the options in myOptions not be be overridden by those in options simply swap the two arguments to Object.assign

Upvotes: 3

Related Questions