Resolving ambiguous and optional function arguments

119 views
Skip to first unread message

Eyal Arubas

unread,
Oct 5, 2014, 1:25:14 PM10/5/14
to nod...@googlegroups.com
I'm wondering what are some common ways to approach the problem of figuring out which arguments the user has passed to a function.

It's a problem I've encountered many times when writing libraries with APIs; especially for functions with optional arguments.
Each such function requires logic to figure out which of the arguments were supplied, and assign default values to those which weren't.
A function with just a few arguments requires a large chunk of code just for this purpose.

An alternative approach is to use an `options` object, the fields of which represent the arguments, and directly assigned by the user.
This, however, imo, is less suitable for public APIs. I prefer to have function signatures with distinct arguments.

It seemed to me like there should be a programmatic solution to this. Each function's arguments should hold certain conditions. Arguments must be of certain types, some are optionals, some have default values, etc.
This led me to develop a module which allows to declare the properties of the function's arguments. The module resolves tnd disambiguates the arguments passed by the user based on those declarations.


Decree works by analyzing a list of argument declarations. It then figures out all the possible combinations.
For example, a function which receives two arguments, with the first one optional, has two possible combinations - the user can call the function with one argument (thus omitting the first one), or with two (thus providing the first one).
A function with two arguments, of which non is optional, has only one combination.
When the user calls the function, decree will match the supplied arguments to a certain combination. If no combination is found, the user supplied invalid arguments. If more than one combination is found, the user was not specific enough, and there is ambiguity. Decree can tell where is the problem.

How do you usually approach this problem?
And what do you think about this solution?

Tom Boutell

unread,
Oct 6, 2014, 9:20:18 AM10/6/14
to nod...@googlegroups.com
It is certainly a legitimate problem, and your solution has elegance going for it. My concern would be the performance overhead.

I do write functions with omittable parameters. But I also tend to limit myself to no more than three positional arguments before introducing an "options object" that takes the rest, unless performance is at a very serious premium (a function called in inner loops thousands of times, for instance).

If I'm allowing omittable parameters, I tend to check arguments.length and make decisions on that basis at the top of a function, repopulating the named parameters of the function with the values "one parameter over" if necessary. In theory this can get complicated, but having many positional parameters is too complicated for the end user to remember anyway so I would never choose to introduce more than one or two optional parameters in the first place.

(There are cases, again in tight loops, where more than three required, positional parameters are unavoidable for performance reasons.)

Adrian Lynch

unread,
Oct 6, 2014, 10:47:48 AM10/6/14
to nod...@googlegroups.com
I like the idea. Have you used it in anger yet? How did it feel in a bigger code base? Did it get in the way or hinder your dev?

We're having the same debate on a new app we're developing and we can't settle on an answer.

Looking forward to more perspective on this...

Adrian

--
Job board: http://jobs.nodejs.org/
New group rules: https://gist.github.com/othiym23/9886289#file-moderation-policy-md
Old group rules: https://github.com/joyent/node/wiki/Mailing-List-Posting-Guidelines
---
You received this message because you are subscribed to the Google Groups "nodejs" group.
To unsubscribe from this group and stop receiving emails from it, send an email to nodejs+un...@googlegroups.com.
To post to this group, send email to nod...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/nodejs/714e2782-1b8b-4203-bcef-36761b65d993%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Jimb Esser

unread,
Oct 6, 2014, 2:06:05 PM10/6/14
to nod...@googlegroups.com
At Cloud Party, we decided to avoid the ambiguity of having optional arguments omitted, and instead took the approach used in other languages where you just explicitly pass null or undefined for the optional arguments.  This had the readability advantage of, given a function signature and a call, you knew exactly which parameter was for which argument without having to know anything about which are optional and which are expected to be which type and what the types are in the caller.  It also allows for slightly stricter code verification (e.g. generate a linter error if any call for a known function is missing an argument).  Refactoring was slightly more work, as you had to actually touch every line of code that called a function when you added a new optional argument - on the other hand, when refactoring Javascript, we've found that it's generally pretty critical to actually inspect every line of code calling your function, so that was not much of a downside.

We also followed a similar rule as stated earlier in the thread - after a function took more than 3-5 arguments, for both readability and maintainability, change it to take an options object instead (or, in a lot of cases where it makes sense, 1 required parameter, an optional options object, and a callback).  Though, more recently, when I'm writing a new function signature, I generally ask myself: "Might I want to add more optional flags/arguments in the future?  If so, might as well just make it take an options argument now".

Though, in performance critical code (if your app has any) performance trumps all other rules, of course =).

Eyal Arubas

unread,
Oct 6, 2014, 2:17:19 PM10/6/14
to nod...@googlegroups.com
Tom,

Performance is indeed a concern.
For each declaration of arguments a function is built by the module (one time). This function is called every time we need to determine the arguments the user passed. Part of the operation of this function is to try to match the user's arguments to some valid combination of arguments. This can be more costly than the regular `if-else` boilerplate code.

I haven't done any benchmarks yet, so I can't say what's the impact on performance, if any.

I agree that more than three optional arguments is not very common, but it's not very rare in my experience.
With 3 optional arguments, the regular boilerplate code of `if-else` statements and `arg2 = arg2 || arg1` is a nuisance, but manageable. With 4, it's a real headache.
It's grunt work, and as such, I wanted a way to automate it and make it simpler for the programmer to focus on writing the function itself; as well as for others reading the code.

I also agree that it's better not to give the user the option to omit too many arguments, but that's a design issue and sometimes unavoidable.

Thanks for the feedback,
Eyal.

Eyal Arubas

unread,
Oct 6, 2014, 2:24:31 PM10/6/14
to nod...@googlegroups.com
Adrian,

My main motivation for writing this module is another module I'm working on; in which I have lot's of API functions which take optional arguments.
I use Decree extensively in that module (you may want to take a look here).
Before using Decree, my functions were simply too big and cumbersome. When reading the code of a function, by the time you got to the real meat, you had to read 10-20 lines of irrelevant code.
As I wrote to Tom above, I imagine there is some performance hit, but I haven't checked yet. I am sure, though, that by making your functions smaller and more focused, they are easier to maintain and debug.

Thanks for the interest,
Eyal.

I should note that both Decree and the other module I mentioned are in beta and have not been used in production. But they are heavily tested.

Axel Kittenberger

unread,
Oct 6, 2014, 3:21:25 PM10/6/14
to nodejs
I agree about the motivation.

Its not even only ambigious and optional argument lists, its even longer argument lists that make a problem, since its often not clear to the callee which argument means what. I never get it, why the order of arguments its considered to be the only argument structuring mechanism to function calls so many languages offer.

Or argument lists that are changing in development or library releases. That can be a pain to get them all correct again.

I know, most people will say, just create an object literal and give that to the function. Albeit this okayish for the occasional function, and more and more standard API functions are adapting to this style or arguments I don't like the idea of creating an object just for a single function call. I know garbage collectors have become awesome, but it still seems unnecessary waste to me, especially if that function is that 10% of code that called often enough to be 90% of runtime already anyway.

What I'm doing now is creating functions that take arguments with alternating string specifier and parameter value.

like: makeCoffee( 'sugars', 7, 'flavor', 'bitter', 'size', 4 );

I call it "free strings" method.

In code I write it like this:
 
makeCoffee(
  'sugars', 7,
  'flavor', 'bitter',
  'size', 4
);

At first I would simple code makeCoffee like this:

function makeCoffee( /* free strings */ )
{
  var
    sugars = 3, // defaultValue
    flavor = 'sweet',
    size = 2;

  for( var a = 0, aZ = arguments.length; a < aZ; a+=2 )
  {
      switch( arguments[ a ] )
      {
      case 'sugars' : 
         // bla
         break;
       ..etc..   
     }
  }

  // actual function block
};

As its true it gets repetitive with not much expressiveness (albeit I believe quite good in runtime), I then tried to code some generic meta-handlers, like you did, but never was satisfied with the result. Again it often resulted in creation of several ultra-short-lived objects on the fly for each function call.

Right now I'm using a code generator to create these repetitive code blocks like aboth. However, its developing while I'm using it and yet not in a state near to be released except eating my own dog food.

To sum it up: I get the problem. I agree it is one. Actually ii is an issue with javascript which it actually shares with most other dynamic languages. Albeit not all! R for example has named arguments. There this whole thing is nicely handled by the language itself -- as far I got it, never coded much beside some "hello world"s in it. I believe a good solution -- next to get ECMA to realize this and alter javascript itself to get named arguments inkl. default values -- would be a meta language to javascript (similar in working than coffeescript) that handles named arguments to compile on the fly to javascript. Like with every metalanguage there comes the issue with debugging, albeit this can be handled nowadays with source maps.

Or just accept and create lots of short lived object literals. Or improve the Javascript runtime so much that a case can be done, that creating ultra short lived object literals and parsing code for it is just as fast as a classical ordered argument list.

Eyal Arubas

unread,
Oct 7, 2014, 3:06:13 AM10/7/14
to nod...@googlegroups.com
Jimb,

I like the idea of having a convention of assigning null / undefined instead of omitting arguments. Refactoring may indeed take more time, but as you implied - it also means more discipline and higher code quality.
However, you can't always educate your users. Your method works great for APIs for which you know who are the users; but with public libraries and modules, your users are most likely not used to those conventions.

Thanks for sharing,
Eyal.

Eyal Arubas

unread,
Oct 7, 2014, 3:33:52 AM10/7/14
to nod...@googlegroups.com
Axel,

Your solution pretty much brings named arguments into Javascript. Pretty cool. First time I'm seeing this approach.

I think that in most other languages which don't have named arguments, users are not allowed to carelessly omit arguments. And if they do, they will encounter a compilation / runtime error before the function even starts to run.
In JS we have the additional burden of validating functions' input. It's a sort of an additional defensive programming we have to do.
Some say, btw, that as long as the docs state how to use the function, it's the user's responsibility to use it correctly.

In JS users are used to calling functions by simply passing the arguments they want. And knowing that the function will just throw an exception if they misused it (a.k.a. "programmer error"). In this context, it might be worth reading this article.
As I mentioned before, any approach which does not follow the regular function-calling paradigm, will have to re-educate the users how to use the functions; which is not easy.
For APIs for which it's known who are the users, it's definitely possible (internal / private libraries). But for public libraries I think we don't always have the luxury of changing our users' habits.

About your solution - I think it's a nice approach. You said you are concerned with creating new objects at runtime due to performance. But any non-trivial solution will incur a performance hit. It should be interesting to do some benchmarks.

Eyal.
Reply all
Reply to author
Forward
0 new messages