Closed TrevorBurnham closed 14 years ago
+1e3
Although I implemented that initial branch, I am not 100% sure it's a good idea any more. If we allow only simple types to be default values then it's OK as you'd have some idea of how a function behaves when called with no/less arguments. On the other hand we can accomplish that using JavaDoc (better support needed) and you would actually get much better hinting in your IDE as well:
###*
* @param {Number} foo The foo, default bar.
###
(foo) ->
foo ?= bar
# do things with foo...
So after a lot of lines of Coffee, leaning towards -1 as I don't really see this as an improvement.
Wow, JavaDoc looks ugly in CS.
I doubt that IDE hinting will be better if a human has to document each implemented default value by hand. For one thing, very few people will do that (even if the support gets there; look at the fate of every JavaScript documentation project); for another thing, it's all too easy for the documentation and the implementation to get out of sync. Putting the default value in the function definition is the only way that there'll ever be reliable IDE support.
As to allowing non-primitive expressions as default argument values, I don't see that as a problem:
(foo = transform bar) ->
# do things with foo...
should mean "if foo
doesn't have a value, then run transform bar
in the context in which the function was defined":
_defaults = [=> transform bar]
(foo) ->
foo ?= _defaults[0]()
# do things with foo...
There are several places in production code where I'd love to use this. It'd be a huge boon in succinctness, readability, and human error reduction.
This is more or less a personal view, but I can't agree a function call in the arguments list is a good idea. I can see function definitions getting two screens wide with JSON objects and function calls... and what not. Lol.
satyr: lurvly, isn't it?
_defaults = [=> transform bar] ...
For what. Can't it be (foo) -> foo ?= transform bar
?
I can't agree a function call in the arguments list is a good idea
There should be a very good reason to do that, yeah.
The most usecases of it will be like (o = {}) -> ...
though.
For what. Can't it be
(foo) -> foo ?= transform bar?
Well, suppose it were @transform
instead, for instance. I want to ensure that code that comes before the ->
takes place outside of the function context, for consistency's sake.
@StanAngeloff While people could surely use the syntax to produce ugly code, I don't think that's a compelling argument for not adding a feature that would enable so much beautiful code. Surely
fill = (x = 0, y = 0, width = canvasWidth, height = canvasHeight) ->
# start drawing...
is preferable to
fill = (x, y, width, height) ->
x ?= 0
y ?= 0
width ?= canvasWidth
height ?= canvasHeight
especially with syntax highlighting to distinguish the argument names from the defaults?
Just thinking of a case:
class Bob
greet: (name = @default()) ->
console.log "Hello #{name}!"
default: -> 'Bob'
bob = new Bob()
bob.greet.call(window)
Is that legit, would you guard @
with a bind?
If we also allow anything to be a default value, I dread seeing this:
func = (next = ((err) -> throw err if err)) ->
# do work...
next null
Hmm. I don't personally feel this is worth it, for CoffeeScript at least. The existing ?= facility seems to be adequate, maps to JavaScript in a straightforward way, and is reasonably self-documenting.
One issue with default values for arguments the question of whether the default value expressions should evaluate in the context of the function definition (as in Python), or the function call itself (as in Ruby). I'm not really convinced that either is the right answer.
Is that legit, would you guard
@
with a bind?
Why? this
in params will be the same as it is in the function.
@satyr: so what if this
changed implicitly or the function is invoked as a callback? If it's the same then the above would be a RuntimeError
since this
was remapped to window
?
Is that legit, would you guard
@
with a bind?
Yes, if necessary. Why not?
And you'd have to be a madman to write
func = (next = ((err) -> throw err if err)) -> ...
instead of
errFunc = (err) -> throw err if err
func = (next = errFunc) -> ...
Again, I don't really see "this feature allows confusing code to be written by terrible coders" as an argument for not including a great feature. Wouldn't you use default arguments frequently in your own code?
If it's the same then the above would be a
RuntimeError
Right. That's the nature of JS we shouldn't bend.
@TrevorBurnham: I don't know to be honest. I've grown really fond of
func = (options) ->
options or= {}
I don't usually like having defaults to begin with and where it's needed it's usually a line or two of code within the body. It's just how I like things structured.
I guess you are right, if it gets abused it's out of our hands. I'd personally not be in a position like that, but frankly we can't allow simple expressions alone.
@Stan what languages do you use other than JS/CS? Don't they have this feature? If they do, would you refuse to use it?
Pretty much everything except Ruby, mostly Coffee/JS these days. Only used it extensively in Python (kwargs too). Never liked it in C# once it got added. Somewhat useful in PHP where you don't have a one-liner to replace it with.
But with all of the above, you can't really go crazy. You can use constants and primitives and that's about it. I am all for that.
Well, as you say Stan, "we can't allow simple expressions alone" due to the nature of JavaScript (e.g. no distinction between variables and constants); so it's all or nothing. Given that choice, I'd much rather go with "all" and continue to wag my finger at anyone who dares to write an unreadable expression in CoffeeScript.
Also, it's funny that you mention or=
. I think this is a fairly common point of confusion:
run (delay) ->
delay or= 100
...
run(0) # will delay for 100
or worse
setVisible (visible) ->
visible or= true
...
setVisible(false) # will setVisible(true)
I'm sure you don't fall into this trap, but I think providing a nice syntax that maps to the lesser-known ?=
will save language newcomers from a common error.
I was going to bring up the same concern as mental: in Python you sometimes end up defining defaults in the function body anyhow, if you don't want the "defined at function definition" behavior for complex types.
As an alternative, I was going to suggest Pattern Matching in combination with the Existential Operator, but after looking at it, I honestly don't think it would improve the syntax.
fill = (x, y, width, height) ->
[x, y, width, height] ?= [0, 0, canvasWidth, canvasHeight]
I don't think x ?= 0; y ?= 0 is so bad, and Stan's pattern also looks quite nice. Especially since those options can be passed in as func(x:0, y:0) without extra squigglies {}.
@TrevorBurnham: we could restrict it to a list of pre-defined primitives we are allowing as well as numbers and strings perhaps (no interpolations however?) So as long as it's a single value and one that is allowed (yes
, no
, true
, false
, etc. NUMBER
, REGEX
, etc.) it should be OK.
fill = (x = 0, y = 0, width = canvasWidth [..snip..]
is a controversial one for me as canvasWidth
is neither a built-in primitive nor a number/string. I guess as long as it's just a name, we can accept it. But no funky function executions:
fill = (x = minX(), y = minY()) ->
or operators:
fill = (x = 0, y = 0, width = canvasWidth + x) ->
or expressions:
fill = (x = 0, y = if x then 100 else 0) ->
we could restrict it to a list of pre-defined primitives we are allowing as well as numbers and strings perhaps
For what exactly? Are you ristricting perfect use cases like this as well?
I think that such restrictions would be unintuitive. They seem to exist in languages like Ruby because function definitions are usually in a context (class definitions) where code execution isn't allowed, so it's not clear what the y
in a statement like def foo(x = y)
would refer to.
I'd prefer for the use complex expressions as default arguments to be considered "bad style" rather than "syntax error."
Are you ristricting perfect use cases like this as well?
I would, yes.
http://github.com/satyr/coffee-script/compare/master...defarg
Anyway, here's the first stab patch which now seems stable.
My 2¢: The whole point of CoffeeScript seems to be making life easier for JS programmers. A nice default arguments syntax is a perfect fit for that.
I don't think it's advisable to add arbitrary restrictions in an attempt to stop people from writing smelly code. The restrictions would just make this feature more complicated and less predictable (in other words, less CoffeeScript-ish). Smelly code will happen either way.
I favor the simple, straightforward translation to foo ?= bar
(or rather, its JS equivalent) that was proposed in the original post.
To muddy the issue slightly: what if this was an extension to pattern matching, rather than function signatures? I really like this existing idiom for named/optional arguments: doThing = (options) -> {color, shape , texture} = options color ?= "red" shape ?= "round" texture ?= "fuzzy"
doThing texture: "smooth"
Would this shortcut satisfy anyone else?: doThing = (options) -> {color ? "red", shape ? "round", texture ? "fuzzy"} = options or possibly doThing = (args...) -> [color ? "red", shape ? "round", texture ? "fuzzy"] = args
+1 for a concise easy-on-the-eyes default arguments syntax. I find myself wishing I could write stuff like (options = {}) ->
every few days.
@sethaurus: I like the look of
{color ? "red", shape ? "round", texture ? "fuzzy"} = options
but I don't see it as a substitute for default arguments. Could you raise it as a separate issue?
I've been playing with the defarg branch, and it's awesome! Thanks, satyr.
Now, I do think that some minor changes are needed in order to provide consistency:
First, as discussed above, arguments that reference this
should be guarded with closures bound to the context in which the function is defined. Here's an example that shows how the unguarded implementation can leave to confusion:
@noise = 'Moo'
makeNoise = (noise = @noise) ->
console.log noise
obj = {}
makeNoise() # 'Moo'
makeNoise.call obj # undefined
Another obvious issue is using argument names in the default argument expressions:
min = 0
max = 100
inNegativeRange = (x, min = -max, max = -min) ->
return min < x and x < max
This is such awful style that I'd like to just throw a syntax error whenever an argument name is used in a default argument expression. Agreed?
Now, I do think that some minor changes are needed in order to provide consistency
Don't think so. As long as you're aware that (x = y) ->
is a sugar for (x) -> x ?= y
, you'll be fine. And since you can look at the generated JS, it's crystal clear what's gonna happen.
CoffeeScript as a language should be able to stand on its own. You shouldn't have to look at the compiled JavaScript in order to understand how a feature works.
Besides, there's a strong aesthetic case here: Expressions that come before ->
should take place outside of the function context. You shouldn't have to think about the way that the CoffeeScript compiler is inserting those expressions into the function.
I realize that it makes the implementation more complex, and from the perspective of someone who works on the compiler, that's unappealing. But it'll save folks from a lot of headaches, and make CoffeeScript a more successful language.
You can simply use =>
if you don't feel like understanding this
correctly.
Expressions that come before -> should take place outside of the function context
Are you suggesting that f = (x = y = z) ->
would compile to something like
var _ref, f, y;
f = (_ref = y = z, function(x) {
x != null ? x : x = _ref;
});
really?
Sure, you could write
makeNoise = (noise = (=> @noise)()) ->
but that's exactly the kind of unreadable function definition that StanAngeloff was so worried about the syntax enabling.
It's only necessary to use a closure if the expression contains a reference to this
. (And perhaps arguments
, ugh.) So, not for (x = y = z) ->
, but for (x = @y = @z)
. Again, the compiled output is messier, but the semantics are much more intuitive and consistent with the syntax.
Put another way: If you were designing a language from scratch, would you make default argument expressions run in whatever context the function is called in, or in the one in which the function is defined?
Parameters belongs to the function. Take a look at other languages that support this feature.
but for
(x = @y = @z)
I really don't understand your reasoning. x
obviously binds to the function, but @y
should be outside? A mess.
Please, make it simple. As Satyr said, it's clear for using, when (x = y) ->
is just (x) -> x ?= y
. Guarding for this
seems excessively too, we can use =>
to bind a function to a context, so:
@noise = 'Moo'
makeNoise = (foo = @noise) =>
console.log foo
Can be translated as:
@noise = 'Moo'
makeNoise = (foo) =>
foo ?= @noise
console.log foo
Beside of that, I have opinion, that guys who use .call
and .apply
know what they do, usually.
I think, everything can be at right part of an default argument, if it'll translate as ?=
statements at the begin of a function and a value of the right part will calculated at runtime.
+1 for full-powered, locally-scoped default parameters.
I doubt most people will write crazy code. I'm sure it'll usually be (x = 10)
or (options = {})
.
Also, another possible syntax: (x: 10, y: 20)-> ...
...which would make this more readable: (x: y = z)-> ...
That said, putting x = y = z
anywhere is gross. :)
@epitron We can't use the syntax you propose because
(x: 10, y: 20)
is legit CoffeeScript shorthand for
({x: 10, y: 20})
As to the main issue, I certainly prefer satyr's implementation to nothing at all. If folks want to adopt more sophisticated semantics in the future, as the syntax becomes widely used, we can always raise a new issue.
+1 I find default arguments very useful.
One question though. The ?=
idiom might be better than or=
, but it's still problematic. It doesn't allow to pass null explicitly. It can be solved by compiling
func = (foo = default) ->
...
into
func = (foo) ->
foo = if arguments.length == 0 then default else foo
Implemented this way default arguments are even easier to justify since writing this by hand is kind of prohibitive.
Or is this not a CS style? Am I the only one who thinks ?=
might cause problems?
@TrevorBurnham I see your point of evaluating default values in the context in which the function was defined and I almost tend to agree. But it's such an infamous gotcha in Python
@akva Hmm, that's an excellent point. In satyr's implementation, (a = x, b = y) ->
compiles to
a != null ? a : a = x;
b != null ? b : b = x;
which is pleasingly efficient; but if I explicitly pass null
, I don't expect it to be changed to something else. akva's proposal could be implemented as
a = (arguments.length > 0) ? a : x;
b = (arguments.length > 1) ? b : y;
This is consistent with the way default arguments work in Ruby, for instance. It's certainly more intuitive. On the other hand, it's actually less versatile, since it means there's no way to call the function f(x, y)
and say "I'd like to provide a value for y
but keep the default value for x
." What do you think, satyr?
a = (arguments.length > 0) ? a : x;
Omitting the first argument by null
ing out and supplying the rest wouldn't be possible with that implementation.
IMO, you shouldn't abuse the distinction between null
and undefined
, considering our existence check purposefully ignore it.
there's no way to call the function f(x, y) and say "I'd like to provide a value for y but keep the default value for x.
Is it possible in Ruby? It's certainly not in Python. Ensuring similarity with existing languages helps avoid surprises.
A good example of explicitly passing null is from node.js
sys.inspect(object, showHidden=false, depth=2)
The default is to only recurse twice. To make it recurse indefinitely, pass in null for depth.
@satyr certainly abusing is not good, but sometimes it makes sense. imho undefined
always means some kind of error or unexpected condition, whereas null
may indicate a special value. After all null
is a valid value in json, while undefined
is not.
@akva Yes, I was saying that it's not possible in Ruby, just as it's not possible in Python or any other language that I know of that has default arguments. satyr may be right that it's unfortunate that null
is used as a significant argument (for instance, in the example that you give, it'd be more sensible to use the Infinity
constant that's baked into JavaScript), but JavaScript coders have been doing so for ages, and I'm afraid that won't change any time soon.
@TrevorBurnham You are right, in this case Infinity
would be better. But how about non numerical values.
Well, let's look at it from another perspective. Let's say
func(
util(), # util has a bug and returns unexpected null
y
)
In which case will it be easier to spot the bug? When buggy null
overrides the default value, or when it doesn't.
In which case will it be easier to spot the bug?
I guess the same question applies to every case an existence check is involved.
Good point, akva. The more I think about this, the more it seems wise to me to stick with precedent and allow null
values to be passed explicitly by making default argument assignment conditional on arguments.length
. 95% of the time, the two are equivalent. The other 5% of the time, the API designer can easily write a ?= x
instead of using the default argument syntax.
The other 5% of the time, the API designer can easily write a ?= x instead of using the default argument syntax.
Good point. Basically arguments.length
gives API designer more flexibility cause then he can choose to use default values or write a ?= x
explicitly.
Using the arguments.length
approach, default arguments behave like other languages, and provide sugar for some pretty verbose code. +1 for this design.
If I'm not mistaken, there hasn't been a substantive discussion of default argument values since issue 49 was closed all the way back in January. There's been enough interest that the issue has been raised at least twice since, and there's been at least one implementation. I think now is a good time to raise the issue afresh, as the language verges on 1.0. Also, the discussion of issue 788, which would provide a keyword for self-executing closures, has hit a stumbling block due to the lack of syntax for providing arguments to functions while defining them.
The main argument against default argument values has been that they clutter function definitions, and that providing "default" values within functions is easy:
Those arguing in favor have emphasized the succinctness of placing the defaults in the function definition, and pointed out that this feature is offered by Ruby, Python, and many other fine languages.
Now there's an additional reason to add default argument values: to provide a syntax for passing values to self-executing closures using the
do
keyword. (Again, see issue 788.) Just as support for YAML-style object construction was sufficient reason to drop the:
assignment syntax, perhaps support fordo
-style closures is sufficient reason to add a default argument syntax.Personally, I favor the Ruby/Python/Groovy/PHP/Scala-esque syntax
as equivalent to
Thoughts?