Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional function arguments (i.e. single-arg splats) #1091

Closed
TrevorBurnham opened this issue Jan 27, 2011 · 25 comments
Closed

Optional function arguments (i.e. single-arg splats) #1091

TrevorBurnham opened this issue Jan 27, 2011 · 25 comments

Comments

@TrevorBurnham
Copy link
Collaborator

Short version

Splats soak up any number of values from 0 to infinity. As a result, you can only use one splat per argument list or array pattern. But there are cases where you want a single optional value, or more than one (potentially non-sequential). Implementing this every time is tedious and increases refactoring effort, making it a high-value language feature.

The proposed syntax is

(required, optional?, additional...) ->

or in array pattern-matching

[first, optional?, additional...] = arr

Single optional arguments would take priority over splatted arguments, with leftward optional arguments taking priority over rightward ones. Optional arguments could be used in conjunction with default arguments, as in

(required, optional ?= {}, additional...) ->

Also, I suggest that the current (arg = default) -> implementation should be deprecated in favor of the more versatile (arg ?= default) syntax.

Uses in function arguments

Let's say that I have a function of the form

report = (options, callback) ->

Having the callback as the last argument is good Node-y style, but options should be, well, optional. So I can either write

report = (options..., callback) ->
  options = options[0]

or

report = (options, callback) ->
  [options, callback] = [{}, options] if typeof options is 'function'

Neither is nearly as clean or self-documenting as

report = (options ?= {}, callback) ->

Let's take another example that uses an optional argument in conjunction with a splatted argument. I'm defining a function where I want at least one value to be in a splatted argument, whenever possible. I have a setTimeout wrapper that takes the form

timeout = (millis, func, args...) ->

and I want millis to default to 0. So all of the following should be valid:

timeout 30000, puts, 'Thanks for waiting'
timeout 500, -> puts 'Goodbye'
timeout -> puts 'Hello'

Currently, implementing this API requires a rather annoying series of swaps:

timeout = (millis, func, args...) ->
  if typeof millis is 'function'
    args.shift func
    func = millis
    millis = 0
  setTimeout (-> func.apply null, args), millis

It gets even worse as you have more arguments, since you have to "shift" each one.

The optional function argument syntax would allow the function to be implemented more readably (and with more efficient JavaScript output) with

timeout = (millis ?= 0, func, args...) ->
  setTimeout (-> func.apply null, args), millis

(Note that this still doesn't take care of the case where a func and args are given but no millis, because this has to be addressed by checking the type of the first argument.)

Note that if you want to ensure that a splatted argument receives at least one value, you should make it a separate argument and concatenate them, e.g.:

attack = (firstTarget, otherTargets..., options ?= {}, callback) ->
  targets = [firstTarget, otherTargets...]

Uses in pattern matching

Honestly, I can't think of any strong use cases in array pattern matching, but I believe the feature should be implemented nonetheless for consistency with splats.

Default values in array pattern matches were previously proposed at issue 810, but rejected as too complex. Unless anyone has a strong use case for it, I think the ?= syntax should only be available for function arguments, not for pattern matching.

Default values: ?= vs. =

For the last argument(s) in a function, the ?= syntax would be somewhat redundant with the existing = syntax for default arguments. There is a subtle difference between them: ?= uses arguments.length, while = uses, er, ?= (confusing, right?), which means that the latter would override a given null or undefined while the former would not.

I think the best way to remove this confusion is to make = and ?= equivalent (the familiar = syntax should probably be kept for the benefit of those coming from other languages), so that there's only one default argument behavior: one that doesn't override explicit null/undefined. That is, both

(required, options ?= {}, callback) ->

and

(required, options = {}, callback) ->

should compile in such a way that if two arguments are given, the second is used as the callback.

Implementation details

We need a length check for each optional value. (I'm going for ease of implementation here. An optimized version would nest the conditionals so that fewer length checks would often be performed.) Assume that arguments.length is assigned to __len. Here's what the existing splat syntax compiles to (breaking each assignment onto its own line):

foo = (a, b...) ->
# a = arguments[0], b = 2 <= __len ? __slice.call(arguments, 1) : [];

foo = (a..., b) ->
# a = 2 <= __len ? __slice.call(arguments, 0, _i = __len - 1) : (_i = 0, []),
# b = arguments[_i++];

foo = (a, b..., c) ->
# a = arguments[0],
# b = 3 <= __len ? __slice.call(arguments, 1, _i = __len - 1) : (_i = 1, []),
# c = arguments[_i++];

Now here's how the implementation would go for optional arguments, with the initial assignment of _i = (index of first optional argument, ignoring splats):

foo = (a, b?) ->
# a = arguments[0],
# b = 2 <= __len ? arguments[_i++] : void 0;

foo = (a?, b) ->
# a = 2 <= __len ? arguments[_i++] : void 0,
# b = arguments[_i++];

foo = (a, b?, c) ->
# a = arguments[0],
# b = 3 <= __len ? arguments[_i++] : void 0,
# c = arguments[_i++];

foo = (a?, b?, c) ->
# a = 2 <= __len ? arguments[_i++] : void 0,
# b = 3 <= __len ? arguments[_i++] : void 0,
# c = arguments[_i++]; 

If a default value is given, substitute it for void 0. Obviously the expressions of the form x = void 0 can be stripped. (That postfix increment on the last _i can also be stripped, but I've kept it here because it's in the current implementation.)

The only changes needed to the splat implementation for it to interoperate would be 1) to use _i as the slice start position if there are preceding optional arguments, and 2) to only change the value of _i in such a case if the splat is non-empty. For all other purposes, optional arguments would work the same way as "required" arguments, since they take priority over splats. Examples:

foo = (a?, b...) ->
# a = 1 <= __len ? arguments[0] : void 0,
# b = 2 <= __len ? __slice.call(arguments, (_i = __len - 1), _i + 1) : [];

foo = (a..., b?) ->
# a = 2 <= __len ? __slice.call(arguments, (_i = __len - 1) - 1, _i) : [],
# b = 1 <= __len ? arguments[_i] : void 0;

foo = (a?, b..., c) ->
# a = 2 <= __len ? arguments[_i++] : void 0,
# b = 3 <= __len ? __slice.call(arguments, (_i = __len - 1) - 1, _i) : [],
# c = arguments[_i++];

foo = (a, b..., c?) ->
# a = arguments[0],
# b = 3 <= __len ? __slice.call(arguments, (_i = __len - 1) - 1, _i) : [],
# c = 2 <= __len ? arguments[_i++] : void 0;

The size of the compiled output should give you some idea of how much effort this feature will save for CoffeeScripters who use it.

What do y'all think?

@TrevorBurnham
Copy link
Collaborator Author

It's worth noting that Node.js uses the style I've described above in its core API. See, for instance, child_process.exec, which takes two required arguments (a command name and a callback) and an optional middle argument (an options hash). The easiest way to implement this in CoffeeScript currently is

exec = (command, options..., callback) ->
  options = options[0]

but the new syntax I'm proposing is both more succinct and, crucially, more self-documenting:

exec = (command, options?, callback) ->

@michaelficarra
Copy link
Collaborator

Since #870 didn't make it (bummer, I know), this one probably should not either. They kind of complement each other.

@TrevorBurnham
Copy link
Collaborator Author

I consider the two unrelated. #870 was proposing a couple of minor syntactic sugars: Being able to write ... instead of foo..., and void instead of [] (in pattern-matching). By contrast, this issue is proposing a major syntactic sugar, one that can turn several lines of hard-to-read code into a couple of punctuation marks (as in the examples above).

@michaelficarra
Copy link
Collaborator

Flagging as notice so that we can invite some discussion about this proposal.

@balupton
Copy link

Would love this. With node.js there is fs.writefile(path,data,encoding,callback) where encoding is an optional argument, been hoping that cofeescript has native support for that - but it doesn't :(

@metaskills
Copy link

I just started learning CoffeeScript and one of my first classes really could use this pattern. I found this page by googling for "coffeescript options hash" so I could learn the standard idiom for this. Trevor's outline seems well thought out and the syntax would have been something I expected to find currently implemented.

@TrevorBurnham
Copy link
Collaborator Author

Thanks, metaskills. There was an alternate proposal at #1225, which I think shows demand for this feature. It would be a fairly major change to the language, though.

@davidchambers
Copy link
Contributor

+1

Splats are awesome; this seems to me a natural complement.

@trans
Copy link

trans commented May 30, 2011

callback is always expected to be a function, yes? If there were a way to state that then the compiler could more intelligently parse the arguments, as in Ruby, where &callback would be used, and it then knows that argument only applies to a trailing block.

So for at least one of your examples:

report = (options, callback) ->
  [options, callback] = [{}, options] if typeof options is 'function'

It could be (actual notation not with standing):

report = (options, &callback) ->

As for the rest I don't really get it, it seems too complicated. But I'm not really fluent in coffee-script yet, so forgive my ignorance if I'm missing the point.

@TrevorBurnham
Copy link
Collaborator Author

@trans That feature was proposed (and rejected) at #1225. It's hard to see how it would interoperate with existing features like splats and default values, methinks. Whereas

report = (options ?= {}, callback) ->

is fairly clear: If you just pass one argument, it's used as callback, and options is set to an empty hash. If you pass two, the first is options and the second is callback.

@trans
Copy link

trans commented May 31, 2011

Hmm... reading about #1225 and your explanation (which helped, thanks) could & not be generalized to mean "parse this from the rear first". Thus:

report = (options = {}, &callback) ->

Would tell it to get callback first then handle options, and the effect would be the same as the ?= suggestion. One could even do more than one.

report = (options = {}, &count = 1, &callback) ->

If one argument is passed it is callback, if two it is count and callback, and more would go to options. Actually I think it might even be reducible to a single marker, since such arguments would necessarily be at the end.

report = (options = {}, & count = 1, callback) ->

Of course, pick whatever symbol would work best if & ins't a good choice.

@brandonbloom
Copy link

I like/want this feature, but I'm concerned about confusion with the existing argument value defaulting behavior.

Consider these two signatures:

fn1 = (x, y = 5, z) ->
fn2 = (x, y ?= 5, z) ->

fn1 requires three arguments to fill z, and there is no way to explicitly call it with null for y.

fn2 however, is sensitive to the arity of the call site. z is set if only two arguments are provided. You can also set y to null by simply providing three arguments.

The syntax described above is especially confusing when you consider that in existing CoffeeScript code, the ?= operator checks for null or undefined, as arity is irrelevant to assignments.

I would seem preferable to swap the meaning of these two, such that = 5 means "optional" argument and ?= 5 means "default if null or undefined". Unfortunately, it's probably way to late for that in terms of code compatibility, so maybe we should consider an alternative syntax.

@TrevorBurnham
Copy link
Collaborator Author

@brandombloom To clarify, this proposal is intended as a replacement for the existing argument value defaulting behavior. This would be a major breaking change, and I'd like it to be considered for CoffeeScript 2.0, which I hope will merit the rethinking of several features.

@mcmire
Copy link

mcmire commented Nov 6, 2011

Perhaps only the fn = (x, y?, z) -> feature could be implemented for now? That's still pretty useful. And if you really want fn = (x, y ?= 5, z) -> you could say fn = (x, y?, z) -> y ?= 5.

@bbuck
Copy link

bbuck commented Mar 26, 2012

The only thing I see against this is that it won't remove type checking or proper variable detection. In the original example it's very easy to see the result of the operation. (options?, callback) -> will guarantee that callback will always be assigned and options only if two parameters are passed. That's easy, but if you introduce @trans's scenario it becomes far less clear.

With the function header: (options?, count?, callback) -> There is a level of confusion what to assign the first of two parameters as. With the call example 2, (() ->) then the function will be assigned to callback as expected, but is 2 going to be options or count. The proposed fix by @trans clears up some of the confusion on that part by using &, but that's already a JavaScript bitwise operator and I don't think it's a good idea to change the way any of the current operators work in CoffeeScript and I think @jashkenas has said that before.

@erisdev
Copy link

erisdev commented Mar 27, 2012

@bbuck the original issue states that the leftmost argument has the highest precedence.

👍 for this, incidentally!

@bbuck
Copy link

bbuck commented Mar 27, 2012

@erisdiscord That still doesn't fix the issue that I presented. If the leftmost argument has precedence then with the given function: (options?, count?, callback) -> and the call to that function example 2, (() ->) then you end up with options set to 2, and empty count variable and the callback. So the solution doesn't completely solve the issue which is essentially type checking optional parameters to verify the contents of each parameter.

@erisdev
Copy link

erisdev commented Mar 27, 2012

@bbuck Oh, duh, I misunderstood this part:

With the call example 2, (() ->) then the function will be assigned to callback as expected, but is 2 going to be options or count.

I suppose the onus would be on you, the programmer, to design an API that avoids those sorts of situations.

@bbuck
Copy link

bbuck commented Mar 27, 2012

Well, is it really a good idea to put in a language construct that you'll have to be consciously aware of when programming to avoid unwanted situations? While I'm not one for oversimplification of things, I also don't think that putting a construct like that in CoffeeScript, a language that is attempting to fix similar situations that JavaScript has with other things, would be the best path for the future.

I don't oppose this feature, despite that I've only pointed out flaws. I just don't think this implementation is complete enough.

@masterspambot
Copy link

+1 for splats

@Soviut
Copy link

Soviut commented Oct 29, 2012

Why not look at Python as a guide. It allows positional arguments to be followed by optional/default arguments and all of them can be set by name. If positional arguments are all referenced by name, or there are no positional arguments, then arguments can be set out of order:

def create_task(name, needs_approval=False):
    print name, needs_approval

# valid
create_task('coffee') # only set positional arg
create_task('coffee', True) # set default arg
create_task('coffee', needs_approval=True) # set default arg by name
create_task(name='coffee', True) # set positional arg by name but default by position
create_task(name='coffee', needs_approval=True) # set both args by name
create_task(needs_approval=True, name='coffee') # set out of order because all args referenced by name

# invalid
create_task() # no positional args
create_task(needs_approval=True, 'coffee') # breaks positioning since not all args are referenced by name

Also, python supports splats for positional args in the form of *args.

def create_tasks(project, *args):
    print project, args # args is a tuple aka immutable array

# valid
create_tasks('new_project')
create_tasks('new_project', 'task1')
create_tasks('new_project', 'task1', 'task2')
# etc.

And default args in the form of **kwargs (keyword args).

def create_tasks(project, *args, **kwargs):
    print name, *args, **kwargs

# valid
create_tasks('new_project')
create_tasks('new_project', 'task1')
create_tasks('new_project', 'task1', 'task2')
create_tasks('new_project', 'task1', deadline='tomorrow')
create_tasks('new_project', 'task1', 'task2', deadline='tomorrow', created_by='Joe')
# etc.

It should be noted that *args and **kwargs are naming conventions. They can be called anything *subtasks or **attributes.

The point is, positional args come first, then default args, then splats.

@jashkenas
Copy link
Owner

Finally read through this, but not convinced. I think that the particular syntax here is fairly confusing -- particularly because of the reordering of the args. Currently:

(a = 1, b) ->

Takes (maybe, this is JavaScript after all) two arguments, the first is a and the second is b. Regardless of the defaults.

Proposed:

(a ?= 1, b) ->

Takes two arguments, or one, in which case the first arg is b. Huh? That's very hard to read -- and not worth the extra line it would otherwise take to implement the behavior.

@shesek
Copy link

shesek commented Mar 9, 2013

In cases where the first arguments are optional, you can get similar functionality by making arguments resolve to the right first, which can be easily done by simply padding the preceding (missing) arguments with nulls with an higher order function:

rargs = (fn) -> (a...) ->
  if (missing = fn.length - a.length) > 0
    nulls = (null for [1..missing])
    fn.apply this, [nulls..., a...]
  else fn.apply this, a

# example
foo = rargs (a, b, c) -> { a, b, c }
foo 1, 2, 3 # -> a: 1, b: 2, c: 3
foo 1, 2    # -> a: null, b: 1, c: 2
foo 1       # -> a: null, b: null, c: 1

# works with default values, too
bar = rargs (a='a', b) -> { a, b }
bar 1    # -> a: 'a', b: 1
bar 1, 2 # -> a: 1, b: 2

@lydell
Copy link
Collaborator

lydell commented Mar 28, 2013

I like this proposal, but I'd like to say that, regardless of syntax, being able to tell how to use a function just by looking at its definition is much more worth than the code it saves, for me.

The problem with this proposal seems to be the syntax, not the idea itself. I have come up with another syntax.

Some documentation use square brackets to denote optional arguments. What if we could adopt that, considering that many are already used to it?

(Note: All my examples are examples of documentation, not function invocations! fn(a, b) is "documentation" for a function such as fn = (a, b) ->.)

As far as I can tell, there are two variations: You either put commas inside or outside the square brackets.

fn([optional], required)
fn(required, [optional])

vs

fn([optional,] required)
fn(required [, optional])

Examples of "outside style":

Express:

app.render(view, [options], callback)

Node:

fs.writeFile(filename, data, [options], callback)

Examples of "inside style":

jQuery:

.on( events [, selector ] [, data ], handler(eventObject) )

PHP:

int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )

"Outside style" won't make it in CoffeeScript, though, since it already is used for destructuring. But "inside style" is currently a syntax error!

PHP also brought up an interesting concept: "Nested optionality". To pass $flags you have to pass $matches, as opposed to the jQuery example, where passing either of selector and data is allowed. In the actual jQuery case they rely on type checking to find which of them was actually passed: If it's a string it's a selector, otherwise it's data. That could possibly be accomplished by annotating the preferred type (like PHP, but we shouldn't do that ;) #2551):

.on( string events [, string selector ] [, object data ], function handler(eventObject) )

If jQuery wanted to only allow data if a selector was passed, this could have been used:

.on( events [, selector [, data ]], handler(eventObject) )

For the other way around:

.on( events [[, selector ], data ], handler(eventObject) )

The nesting thing wasn't anything I considered initially. Is it too complex?

As a downside, the syntax might look weird coupled with array destructuring:

fn([[optional1, optional2],] required)
fn(required [, [optional1, optional2]])

And what about single-argument functions?

fn([optional]) # Actually means destructuring!

However, in that case a default value could simply have been provided:

fn(optional = null)

So this would be backwards-compatible (I hope!), but might be confusing if you mix default values and optional arguments:

fn(a = 1 [, b = 2])

What does that mean? Well, it's equivalent to

fn(a = 1, b = 2)

or

fn([a = 1,] [b = 2])

or

fn([a = 1] [, b = 2])   

I don't know about compilation and the finer details though ... Let's discuss ;)

@Soviut
Copy link

Soviut commented Mar 28, 2013

@jashkenas your example

(a=5, b) ->

wouldn't work in Python since a=5 would be a keyword argument, and b would be a positional argument. The order python uses is positional args first, then splat args *args, then splat keyword args **kwargs.

So perhaps this alleviates some of your worry about confusing out of order args when calling functions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests