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

Multiple existential operators work weirdly. #2199

Closed
KamilaBorowska opened this issue Mar 17, 2012 · 27 comments
Closed

Multiple existential operators work weirdly. #2199

KamilaBorowska opened this issue Mar 17, 2012 · 27 comments
Labels

Comments

@KamilaBorowska
Copy link

variable = a ? b ? ':-)'
console.log variable

This code returns "ReferenceError: Undefined variable: b" in CoffeeScript (I've tested 1.2.0 and 1.2.1-pre).

This issue doesn't happen in Coco. Also, following code doesn't cause this issue.

variable = a ? (b ? ':-)')
console.log variable
@michaelficarra
Copy link
Collaborator

Our binary existential operator associates to the left. a ? b ? c is equivalent to (a ? b) ? c. We should probably have it associate to the right instead like and and or.

@erisdev
Copy link

erisdev commented Mar 17, 2012

👍 for right associativity. OP isn't the only person who expected it to behave like and or or!

@KamilaBorowska
Copy link
Author

Not that CoffeeScript follows Perl, but in Perl // (defined-or) operator associates to the right left. And this behavior makes way more sense than what CoffeeScript does.

[glitchmr@empire /]# perl -MO=Deparse,p -e'print $a // $b // $c'
print(($a // $b) // $c);
-e syntax OK
```</del>

@erisdev
Copy link

erisdev commented Mar 17, 2012

@glitchmr I wouldn't be surprised if the existential operator were actually inspired by Perl's //, although that output actually appears to imply that it's left-associative.

Perl's semantics also differ from CoffeeScript's. With use strict, the // with an undeclared variable to the left is an error, where in CoffeeScript it isn't. Try it with variables other than $a and $b because they're special.

Nautilus% perl -Mstrict -e 'my $c = "see"; print $undef_a // $undef_b // $c'
Global symbol "$undef_a" requires explicit package name at -e line 1.
Global symbol "$undef_b" requires explicit package name at -e line 1.
Execution of -e aborted due to compilation errors.

@KamilaBorowska
Copy link
Author

Yeah, you're right. But, because of short-circuit evaluation property of ? operator, except of this edge case, everything should work properly after doing change (unless something has this same priority as ? operator)

(yeah, you can tell I don't understand concepts on associativity :P)

@csubagio
Copy link

Well, it's tempting to read that statement as v = a or b or c, with the assumption that c is the desired default value.

If ? were an || 'or' operator like Javascript's (that returned the truthy value, rather than a boolean), this would work fine: the chain would evaluate left to right and shortcircuiting would get you the first truthy result. You'd also get to be smug that no further processing would (should) occur. So if you did a v = cache || getStuff() you'd assume that you'd not call the stuff if you had the cache.

That is assuming that all the variables you need to test exist: Javascript won't accept an undefined reference in there. Which is where ? comes in.

Unfortunately while the ? is smarter and checks for a existing, thereby allowing it to skip a to get to b, it doesn't care to check for b existing, which leads to not just the chaining problem above, but to the failure of the simpler case v = a ? b when neither a nor b exist.

So: I don't think making ? right associative is the answer, not only because all the references might not exist anyway, but because it breaks the shortcircuit assumption that I'd be inclined to have. Instead, how about ? be made to check both operands for existance and return undefined if it finds them to be so?

That way any number of chains will work, and we'll still do the least amount of work to get to it.

@josher19
Copy link
Contributor

If making ? left associative is a "breaking change", then
I'd recommend that making a new ?? operator that works like ||

Global = module ?? window ?? global ?? this

which becomes

Global = module ? ( window ? ( global ? this ) )

which compiles to this Javascript (unless variables are already defined and it uses != null instead):

Global = typeof module !== "undefined" && module !== null ? module : typeof window !== "undefined" && window !== null ? window : typeof global !== "undefined" && global !== null ? global : this;

and works kind of like you'd expect Global = module || window || global || this to work if Javascript didn't throw ReferencErrors on undefined variables (in this case we don't have to worry about falsy values like false, 0, and '').

Why another operator? Right now ? is doing double and triple duty. At first glance, I'd expect these to be the same:

a = b ? 'c'
a = b? 'c'

but in fact, they are not:

a = typeof b !== "undefined" && b !== null ? b : 'c';
a = typeof b === "function" ? b('c') : void 0;

and these two are the same:

a = if b? then b else 'c'
a = b ? 'c'

@KamilaBorowska
Copy link
Author

Except that:

a? b
a ? b

This is not really a problem. Many more operators are context sensitive like this. To give examples, + (unary plus or addition (of string or numbers)), - (negation or substraction), / (divide by or regexp), [] (array or array access), () (function call or just grouping).

Those four examples below are different. But it's sort of intuitive.

func (a) + b
func(a) + b
func (a) +b
func(a) +b

@jashkenas
Copy link
Owner

I'm confused here, because as far as I can tell -- the logical operators and and or are also defined to be left associative ... as they are in logic as well as in our grammar. What exactly do we need to change here?

@michaelficarra
Copy link
Collaborator

Binary ? operator's associativity. We would like it to be right-associative: a ? b ? c should be a ? (b ? c). Currently:

$ coffee -ne 'a ? b ? c'
Block
  Op ?
    Op ?
      Value "a"
      Value "b"
    Value "c"
$ 

@jashkenas
Copy link
Owner

Sorry to be repetitive, but why do we want it to be right-associative? In logic, and in JavaScript, logical operators are left-associative.

https://developer.mozilla.org/en/JavaScript/Reference/Operators/Operator_Precedence

@michaelficarra
Copy link
Collaborator

$ coffee -bpe '(a, b) -> console.log (a ? b ? c)'
(function(a, b) {
  var _ref;
  return console.log((_ref = a != null ? a : b) != null ? _ref : c);
});
$ coffee -bpe '(a, b) -> console.log (a ? (b ? c))'
(function(a, b) {
  return console.log(a != null ? a : b != null ? b : c);
});
$ 

edit: Comment removed. See @josher19's comment below for the more important example.

@josher19
Copy link
Contributor

Problem is that:

variable = a ? b ? 'default'

becomes

variable = ((a ? b) ? 'default')

compiles to buggy:

variable = (_ref = typeof a !== "undefined" && a !== null ? a : b) != null ? _ref : 'default';

which throws a ReferenceError for undefined b, when you'd expect it to be equivalent to coffeescript:

variable = (a ? (b ? 'default'))

which correctly compiles to:

variable = typeof a !== "undefined" && a !== null ? a : typeof b !== "undefined" && b !== null ? b : 'default';

and gets the correct answer of 'default'.

->> Josh W <<-

@josher19
Copy link
Contributor

In Javascript, the conditional operator (?:) is right-to-left, and since the binary existential operator compiles to a conditional operator, it really should be right-to-left. Or if that will break code, you could create a '??' operator which is right-to-left.

@jashkenas
Copy link
Owner

I'm afraid I don't see how it's more useful or expected if the expected thing is to be left associative. If when I look at a or b or c, I read:

(a or b) or c.

... then when I see a ? b ? c, I read:

(a ? b) ? c

.. if a doesn't exist then b, and if the value of all that still doesn't exist then c. I think changing this would break logical consistency.

@michaelficarra
Copy link
Collaborator

@jashkenas: I think you should take a look at those compilations again. See that they both align with the behaviour you expect in the case that the first two operands are declared. But in the case that the first operand is null or undeclared and the second operand is undeclared, the suggested compilation succeeds while the current compilation fails. I don't think this is a matter of opinion here -- the right-associative compilation is always superior.

@jashkenas
Copy link
Owner

It's not a matter of success or failure, or superior and inferior ... it's a matter of different semantics. Take this example:

1 ? null and 2

If ? is left-associative, as it is now, the result of this expression is (1 ? null) and 2 is 1 and 2 is 2

If ? is right-associative, the result of this expression is 1 ? (null and 2) is 1 ? null is 1

Leaving it left-associative is consistent with the rest of our logical operators.

@michaelficarra
Copy link
Collaborator

That's only if it's at the same precedence level. If it's higher precedence than and (which is already higher precedence than or), the behaviour is as you expect: (a ? b) and c

@jashkenas
Copy link
Owner

In terms of CoffeeScript precedences, they're all the same -- LOGIC. If you feel strongly about this, feel free to change it.

@michaelficarra
Copy link
Collaborator

Oh, wow, you're right. I never noticed that. That's probably very unexpected behaviour for people coming from... pretty much anything else. Have there been issues discussing that?

@jashkenas
Copy link
Owner

No -- because it doesn't make a semantic difference in our case (we're not adding parentheses where none are required). The underlying JavaScript order of operations really rule the day.

@michaelficarra
Copy link
Collaborator

Ah, I see. I just looked at the parse tree and was shocked. But when it's output without grouping, JS precedence takes over. Got it.

@satyr
Copy link
Collaborator

satyr commented Apr 26, 2012

@josher19: since the binary existential operator compiles to a conditional operator, it really should be right-to-left

Agreed.

@jashkenas: it doesn't make a semantic difference in our case (we're not adding parentheses where none are required)

Right. This is why I was able to change LOGIC to right-associative with minimal changes to and/or behavior. AST order is merely compilation order.

@michaelficarra
Copy link
Collaborator

This is why I was able to change LOGIC to right-associative with minimal changes to and/or behavior

I think that's a smart move for CoffeeScript. @jashkenas: Will you consider that?

@jashkenas
Copy link
Owner

Sure -- if y'all think it's the better thing to do, keeping in mind that in logic proper, these operators are left associative.

@josher19
Copy link
Contributor

Instead of making all of LOGIC right associative you could make TRI with lower precedence and right-associative, and have a ? b and perhaps other things that compile to (right-associative) conditional javascript operator :? go in that group.

I could help make a test suite from existing issues which mention "Existential" or you could just test against a few big Coffeescript projects on npm or github as a "representative sample" and see if things break.

If it looks like code might break you should either make it a v1.4 milestone with ample warning or create a new a ?? b operator (see: issues #1288 , #92) with right-assoc behavior and leave a ? b as current left-assoc behavior, and encourage people to use a ?? b ?? c instead of a ? b ? c in the docs. That might also help avoid the minor "false friend" of Javascript developers getting surprised when a ? b : c doesn't compile like they were expecting.

Let me know if there is anything else I can do to help.

->> Josh <<-

@GeoffreyBooth
Copy link
Collaborator

A pull request for this would be considered, but I’m not sure it’s worth doing at this point due to the breaking change it would cause.

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

No branches or pull requests

9 participants