Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Implement #156, Added expansion to destructuring #3268

Merged
merged 1 commit into from

9 participants

@xixixao

Implements #156 (oldest open issue currently). Namely @epidemian's proposal (same as #870).

[first, middle..., last] = array # works today
[first, ..., last] = array # works with this PR

[..., last] = ratherLongVariableName
last = ratherLongVariableName[ratherLongVariableName.length - 1] # rather long
[..., last] = a
last = a[a.length - 1] # still longer, repetitive 

This matches nicely the ability of taking first n elements out of an array.

[first, ..., last] = tuple # first = tuple[0], last = tuple[tuple.length - 1];

edited to match final PR

@davidchambers
[first, middle..., last] = array # works today
[first, .., last] = array # works with this PR

It seems to me the elision should be represented by ... rather than .., to match middle....

@michaelficarra
Collaborator

I'm pretty sure we decided against this in #870, opting to recommend [first, [], last] = array. Also, for pulling off the last element without mentioning the list more than once, I usually use something like this: generateList()[-1..][0]. It's not pretty, but it works.

@lydell

@michaelficarra you mean [first, []..., last] = array, right? Your code means [first, [], third] = array.

Also note that {} can be used in place of []: [first, {}..., last] = array, [first, {}, third] = array.

@xixixao

@davidchambers I used .. because ... is already used for splats, and this is something different. Plus it's shorter.

@michaelficarra generateList()[-1..][0] is awefully inefficient (I have seen the code around as well, such strong is the urge of not repeating the array). This PR can compile straight to array[array.length - 1]. I think #870 got side tracked by talking about empty assignments.

@lydell You are correct, but the compiled code is still inefficient:

[first, []..., last] = a # first = a[0], 3 <= a.length ? __slice.call(a, 1, _i = a.length - 1) : (_i = 1, []), last = a[_i++];
#vs
[first, .., last] = a # first = a[0], last = [a.length - 1]

We could open up an issue for the compilation and fix it, but imho this syntax is utterly ugly and non-intuitive. Mind that #870 did not offer the change as a solution to #156. @jashkenas ?

@vendethiel
Collaborator

Seems to me that splat should work here, without the need for [] before (coco allows that). Can you explain why you think it's different ?

@xixixao

@Nami-Doc Because splat never stands on its own, it always serves as either assignment target or application.

(a, splat...) -> # splat is assigned
fn splat... # splat is applied
[.., last] = a # do not assign the values until the last one
               # "stretch the empty space" or "shift the assigned index towards end" 

I think this should be completely disallowed, see the nonsensical output:

[[]...] = a # var __slice = [].slice; 1 <= a.length ? __slice.call(a, 0) : [];

I see how splats make sense coming from that syntax, but I think given no one actually knows it, it doesn't make the syntax more intuitive. But if that's the only problem stopping us, happy to go for one more dot.

@erisdiscord

@xixixao there's actually a problem with your proposed "efficient" compilation here. What happens when a isn't at least two elements?

a = ['first']
[first, .., last] = a # first = a[0], last = [a.length - 1]

first # => 'first'
last # => 'first'

Even crazier hypothetical case:

a = []
[first, ..., last] = a

first # => undefined
last # => ???

OK, in this case, last would be undefined, but if for some ungodly reason there were a -1 property assigned elsewhere… This is more of an obscure corner case that should never happen, but it makes a good demonstration I think.

@xixixao

@erisdiscord
The first is a good catch. I think both compilations make sense. We could check for the length in such case. Or we could leave it as it is, after all, what is the last element in the array a?

I don't think the second is a problem:

a = []
a[-1] = 'I am a devil'
last = a[a.length - 1] # well, that really isn't our fault
@xixixao

I have more time now again, so I could finish this up. Do we want this? @Nami-Doc ?

src/nodes.coffee
@@ -1236,12 +1237,21 @@ exports.Assign = class Assign extends Base
val += ") : []"
val = new Literal val
splat = "#{ivar}++"
+ else if not expansion and obj instanceof Expansion
@vendethiel Collaborator

maybe we should check here for multiple expansions. Minor nitpick though ...

@xixixao
xixixao added a note

I just copied the way Splats are handled. Would you change both? (I don't remember whether there's a reason for the deferral of the error to the last block).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@vendethiel
Collaborator

I still believe ... makes more sense than ..; but I'm not one to decide

@lydell

I like this, because the current way is just a hack. Compare the following two:

[first, []..., last] = array
# Destructure `array`. Save its first value into `first`. Soak up the second til the next-to-last
# value (if any) into an array, but don't save it to a variable. Instead, immediately
# destructure that array. However, don't save any of its values to any variable. Which is like
# skipping to the last value of the initial array. Save that value to `last`. Phew!
[first, .., last] = array
# Desctructure `array`. Save its first value in `first`. Skip to the last value. Save that value to
# `last`. Done.

Moreover, it provides a solution to that old "I wish array[-1] returned the last element of the array" problem. Instead of using first = array[0] and last = array[-1], we can use the much nicer [first] = array and [.., last] = array, or together: [first, .., last] = array.

As for .. vs ...: Doesn't matter to me.

@bjmiller

My vote would be for three dots. In my head, three dots is "ellipsis/splat/soak", and two dots is "go back a level".

@xixixao

@bjmiller Why "go back a level"? @lydell That's exactly my thinking.

So I polished this up, fixed error handling and optimized the simple compilation:

[first, .., last] = tuple # first = tuple[0], last = tuple[tuple.length - 1];

@jashkenas ?

@erisdiscord

@xixixao because of POSIX paths I'd assume

@bjmiller

@xixixao: @erisdiscord is correct. Ever since my very first Unix login long ago, ".." has been burned into my brain as the way to go up the directory tree.

@xixixao

Ah, I even had alias ..="cd .." in bash (fish doesn't even need it). I don't think that's really relevant here though (like the meaning of square brackets say).

In CS, two dots currently have two meanings: [a..b] inclusive range and [a..] expands range to the end. The second one is where I get my reasoning for two dots, but it really doesn't matter much, I like it for being shorter.

@xixixao xixixao referenced this pull request
Closed

release 1.6.4 #3141

@jashkenas
Owner

My thoughts: If y'all want this, it should definitely be three dots instead of two — an ellipsis is a real thing. And it should work as a standalone token in any reasonable circumstance. That means the here-suggested:

[a, ..., b] = list

... as well as things like this:

func = (first, ..., last) ->

func = (..., last) ->

... and so on.

Another thing to think about, syntactically, is the strangeness / ugliness of an ellipsis followed by a comma, or preceded by one: , ..., as part of your line of code. One possible alternative is allowing the commas to be dropped:

[a ... b] = list

func = (first ... last) ->

Just a notion.

@epidemian

@jashkenas,

Another thing to think about, syntactically, is the strangeness / ugliness of an ellipsis followed by a comma, or preceded by one: , ..., as part of your line of code.

I think that notation is pretty natural, at least in a mathematical context. For example, take a look at some of the sequences mentioned in the Wikipedia article for sequence:

For instance, the sequence of the first 10 square numbers could be written as

:smiley_cat:

@xixixao
[first, second...butOne, last] = a
[first, second, ..., butOne, last] = a

Latter reads better for me. Secondly, as I mentioned, it would be really hard to parse if it coincided with ranges.

@vendethiel
Collaborator

Agreed, I'd really prefer if we kept the comma mandatory

@jashkenas
Owner

Agreed, I'd really prefer if we kept the comma mandatory

Ok. Sounds good. Is it ready to merge, then?

src/nodes.coffee
@@ -1206,7 +1206,7 @@ exports.Assign = class Assign extends Base
vvar = value.compileToFragments o, LEVEL_LIST
vvarText = fragmentsToText vvar
assigns = []
- splat = false
+ expandedIdx = false
@vendethiel Collaborator

if we can't keep the align, we probably should keep only one space

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@vendethiel
Collaborator

@jashkenas If it works with argument lists as you said, I'd say yes (once the sigil is fixed). Glad to see that in

@xixixao

Ready to squash @Nami-Doc ?

src/nodes.coffee
((11 lines not shown))
if p.this then p = p.properties[0].name
if p.value then o.scope.add p.value, 'var', yes
splats = new Assign new Value(new Arr(p.asReference o for p in @params)),
new Value new Literal 'arguments'
break
for param in @params
+ debugger
@vendethiel Collaborator

whoops ;)

@xixixao
xixixao added a note

Ha :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
test/functions.coffee
@@ -178,6 +178,16 @@ test "default values with splatted arguments", ->
eq 1, withSplats(1,1,1)
eq 2, withSplats(1,1,1,1)
+test "#156: parameter lists with expansion", ->
+ a = (first, ..., lastButOne, last) ->
+ eq 1, first
+ eq 4, lastButOne
+ last
+ eq 5, (a 1, 2, 3, 4, 5)
+
+ throws (-> CoffeeScript.compile "(..., a, b...) ->"), null, "prohibit expansion and a splat"
+ throws (-> CoffeeScript.compile "(...) ->"), null, "prohibit lone expansion"
@vendethiel Collaborator

I'd maybe add one other case containing both ((..., [...b]) ->. Not sure though ...

@xixixao
xixixao added a note

Great catch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@xixixao

Now should be ready.

@jashkenas jashkenas merged commit daa6ad5 into from
@michaelficarra
Collaborator

What are we going to do about the single-argument-skipping syntax? Just yesterday I had to use this ugly hack:

[[], commit, timestamp, author] = stdout.match /([A-F\d]{40}) (\d+) ([^@\s]+@[^@\s]+)/i
@jashkenas
Owner

What are we going to do about the single-argument-skipping syntax?

Just name it?

@michaelficarra
Collaborator

Gross. That makes it look like you mean to use it later, and will cause linting tools to (perfectly legitimately) complain about an unused variable.

@xixixao

I agree that is ugly, and in future I'd like to make [] and {} and their splats not assignable. Also I missed out a case, so there is a sibling PR coming.

Btw I had a similar case as Michael's above when writing the new error logic in coffee-script.coffee.

@michaelficarra
Collaborator

@xixixao: Do you mind opening an issue for it and summarising the opinions presented within the now closed #870? I'd rather not lose track of it, especially before it ever had a dedicated issue.

@xixixao

Feel free to go ahead, I might get to it later but 1.7.0 is a priority now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 24, 2014
  1. @xixixao
This page is out of date. Refresh to see the latest.
View
8 lib/coffee-script/grammar.js
@@ -148,6 +148,8 @@
return new Param($1, null, true);
}), o('ParamVar = Expression', function() {
return new Param($1, $3);
+ }), o('...', function() {
+ return new Expansion;
})
],
ParamVar: [o('Identifier'), o('ThisProperty'), o('Array'), o('Object')],
@@ -327,7 +329,11 @@
return $1.concat($4);
})
],
- Arg: [o('Expression'), o('Splat')],
+ Arg: [
+ o('Expression'), o('Splat'), o('...', function() {
+ return new Expansion;
+ })
+ ],
SimpleArgs: [
o('Expression'), o('SimpleArgs , Expression', function() {
return [].concat($1, $3);
View
62 lib/coffee-script/nodes.js
@@ -1,6 +1,6 @@
// Generated by CoffeeScript 1.6.3
(function() {
- var Access, Arr, Assign, Base, Block, Call, Class, Code, CodeFragment, Comment, Existence, Extends, For, HEXNUM, IDENTIFIER, IDENTIFIER_STR, IS_REGEX, IS_STRING, If, In, Index, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, Literal, METHOD_DEF, NEGATE, NO, NUMBER, Obj, Op, Param, Parens, RESERVED, Range, Return, SIMPLENUM, STRICT_PROSCRIBED, Scope, Slice, Splat, Switch, TAB, THIS, Throw, Try, UTILITIES, Value, While, YES, addLocationDataFn, compact, del, ends, extend, flatten, fragmentsToText, isLiteralArguments, isLiteralThis, last, locationDataToString, merge, multident, parseNum, some, starts, throwSyntaxError, unfoldSoak, utility, _ref, _ref1,
+ var Access, Arr, Assign, Base, Block, Call, Class, Code, CodeFragment, Comment, Existence, Expansion, Extends, For, HEXNUM, IDENTIFIER, IDENTIFIER_STR, IS_REGEX, IS_STRING, If, In, Index, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, Literal, METHOD_DEF, NEGATE, NO, NUMBER, Obj, Op, Param, Parens, RESERVED, Range, Return, SIMPLENUM, STRICT_PROSCRIBED, Scope, Slice, Splat, Switch, TAB, THIS, Throw, Try, UTILITIES, Value, While, YES, addLocationDataFn, compact, del, ends, extend, flatten, fragmentsToText, isLiteralArguments, isLiteralThis, last, locationDataToString, merge, multident, parseNum, some, starts, throwSyntaxError, unfoldSoak, utility, _ref, _ref1,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
@@ -1637,7 +1637,7 @@
};
Assign.prototype.compilePatternMatch = function(o) {
- var acc, assigns, code, fragments, i, idx, isObject, ivar, name, obj, objects, olen, ref, rest, splat, top, val, value, vvar, vvarText, _i, _len, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7;
+ var acc, assigns, code, expandedIdx, fragments, i, idx, isObject, ivar, name, obj, objects, olen, ref, rest, top, val, value, vvar, vvarText, _i, _len, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7;
top = o.level === LEVEL_TOP;
value = this.value;
objects = this.variable.base.objects;
@@ -1669,7 +1669,7 @@
vvar = value.compileToFragments(o, LEVEL_LIST);
vvarText = fragmentsToText(vvar);
assigns = [];
- splat = false;
+ expandedIdx = false;
if (!IDENTIFIER.test(vvarText) || this.variable.assigns(vvarText)) {
assigns.push([this.makeCode("" + (ref = o.scope.freeVariable('ref')) + " = ")].concat(__slice.call(vvar)));
vvar = [this.makeCode(ref)];
@@ -1689,7 +1689,7 @@
}
}
}
- if (!splat && obj instanceof Splat) {
+ if (!expandedIdx && obj instanceof Splat) {
name = obj.name.unwrap().value;
obj = obj.unwrap();
val = "" + olen + " <= " + vvarText + ".length ? " + (utility('slice')) + ".call(" + vvarText + ", " + i;
@@ -1700,14 +1700,26 @@
val += ") : []";
}
val = new Literal(val);
- splat = "" + ivar + "++";
+ expandedIdx = "" + ivar + "++";
+ } else if (!expandedIdx && obj instanceof Expansion) {
+ if (rest = olen - i - 1) {
+ if (rest === 1) {
+ expandedIdx = "" + vvarText + ".length - 1";
+ } else {
+ ivar = o.scope.freeVariable('i');
+ val = new Literal("" + ivar + " = " + vvarText + ".length - " + rest);
+ expandedIdx = "" + ivar + "++";
+ assigns.push(val.compileToFragments(o, LEVEL_LIST));
+ }
+ }
+ continue;
} else {
name = obj.unwrap().value;
- if (obj instanceof Splat) {
- obj.error("multiple splats are disallowed in an assignment");
+ if (obj instanceof Splat || obj instanceof Expansion) {
+ obj.error("multiple splats/expansions are disallowed in an assignment");
}
if (typeof idx === 'number') {
- idx = new Literal(splat || idx);
+ idx = new Literal(expandedIdx || idx);
acc = false;
} else {
acc = isObject && IDENTIFIER.test(idx.unwrap().value || 0);
@@ -1834,17 +1846,22 @@
_ref3 = this.params;
for (_i = 0, _len = _ref3.length; _i < _len; _i++) {
param = _ref3[_i];
- o.scope.parameter(param.asReference(o));
+ if (!(param instanceof Expansion)) {
+ o.scope.parameter(param.asReference(o));
+ }
}
_ref4 = this.params;
for (_j = 0, _len1 = _ref4.length; _j < _len1; _j++) {
param = _ref4[_j];
- if (!param.splat) {
+ if (!(param.splat || param instanceof Expansion)) {
continue;
}
_ref5 = this.params;
for (_k = 0, _len2 = _ref5.length; _k < _len2; _k++) {
p = _ref5[_k].name;
+ if (!(!(param instanceof Expansion))) {
+ continue;
+ }
if (p["this"]) {
p = p.properties[0].name;
}
@@ -2037,7 +2054,7 @@
} else {
iterator(obj.base.value, obj.base);
}
- } else {
+ } else if (!(obj instanceof Expansion)) {
obj.error("illegal parameter " + (obj.compile()));
}
}
@@ -2117,6 +2134,29 @@
})(Base);
+ exports.Expansion = Expansion = (function(_super) {
+ __extends(Expansion, _super);
+
+ function Expansion() {
+ return Expansion.__super__.constructor.apply(this, arguments);
+ }
+
+ Expansion.prototype.isComplex = NO;
+
+ Expansion.prototype.compileNode = function(o) {
+ return this.error('Expansion must be used inside a destructuring assignment or parameter list');
+ };
+
+ Expansion.prototype.asReference = function(o) {
+ return this;
+ };
+
+ Expansion.prototype.eachName = function(iterator) {};
+
+ return Expansion;
+
+ })(Base);
+
exports.While = While = (function(_super) {
__extends(While, _super);
View
276 lib/coffee-script/parser.js
140 additions, 136 deletions not shown
View
2  src/grammar.coffee
@@ -217,6 +217,7 @@ grammar =
o 'ParamVar', -> new Param $1
o 'ParamVar ...', -> new Param $1, null, on
o 'ParamVar = Expression', -> new Param $1, $3
+ o '...', -> new Expansion
]
# Function Parameters
@@ -378,6 +379,7 @@ grammar =
Arg: [
o 'Expression'
o 'Splat'
+ o '...', -> new Expansion
]
# Just simple, comma-separated, required arguments (no fancy syntax). We need
View
50 src/nodes.coffee
@@ -1203,10 +1203,10 @@ exports.Assign = class Assign extends Base
if obj.unwrap().value in RESERVED
obj.error "assignment to a reserved word: #{obj.compile o}"
return new Assign(obj, value, null, param: @param).compileToFragments o, LEVEL_TOP
- vvar = value.compileToFragments o, LEVEL_LIST
+ vvar = value.compileToFragments o, LEVEL_LIST
vvarText = fragmentsToText vvar
- assigns = []
- splat = false
+ assigns = []
+ expandedIdx = false
# Make vvar into a simple variable if it isn't already.
if not IDENTIFIER.test(vvarText) or @variable.assigns(vvarText)
assigns.push [@makeCode("#{ ref = o.scope.freeVariable 'ref' } = "), vvar...]
@@ -1225,7 +1225,7 @@ exports.Assign = class Assign extends Base
[obj, idx] = new Value(obj.unwrapAll()).cacheReference o
else
idx = if obj.this then obj.properties[0].name else obj
- if not splat and obj instanceof Splat
+ if not expandedIdx and obj instanceof Splat
name = obj.name.unwrap().value
obj = obj.unwrap()
val = "#{olen} <= #{vvarText}.length ? #{ utility 'slice' }.call(#{vvarText}, #{i}"
@@ -1235,13 +1235,23 @@ exports.Assign = class Assign extends Base
else
val += ") : []"
val = new Literal val
- splat = "#{ivar}++"
+ expandedIdx = "#{ivar}++"
+ else if not expandedIdx and obj instanceof Expansion
+ if rest = olen - i - 1
+ if rest is 1
+ expandedIdx = "#{vvarText}.length - 1"
+ else
+ ivar = o.scope.freeVariable 'i'
+ val = new Literal "#{ivar} = #{vvarText}.length - #{rest}"
+ expandedIdx = "#{ivar}++"
+ assigns.push val.compileToFragments o, LEVEL_LIST
+ continue
else
name = obj.unwrap().value
- if obj instanceof Splat
- obj.error "multiple splats are disallowed in an assignment"
+ if obj instanceof Splat or obj instanceof Expansion
+ obj.error "multiple splats/expansions are disallowed in an assignment"
if typeof idx is 'number'
- idx = new Literal splat or idx
+ idx = new Literal expandedIdx or idx
acc = no
else
acc = isObject and IDENTIFIER.test idx.unwrap().value or 0
@@ -1336,10 +1346,10 @@ exports.Code = class Code extends Base
delete o.isExistentialEquals
params = []
exprs = []
- for param in @params
+ for param in @params when param not instanceof Expansion
o.scope.parameter param.asReference o
- for param in @params when param.splat
- for {name: p} in @params
+ for param in @params when param.splat or param instanceof Expansion
+ for {name: p} in @params when param not instanceof Expansion
if p.this then p = p.properties[0].name
if p.value then o.scope.add p.value, 'var', yes
splats = new Assign new Value(new Arr(p.asReference o for p in @params)),
@@ -1453,7 +1463,7 @@ exports.Param = class Param extends Base
atParam obj
# * simple destructured parameters {foo}
else iterator obj.base.value, obj.base
- else
+ else if obj not instanceof Expansion
obj.error "illegal parameter #{obj.compile()}"
return
@@ -1504,6 +1514,22 @@ exports.Splat = class Splat extends Base
concatPart = list[index].joinFragmentArrays args, ', '
[].concat list[0].makeCode("["), base, list[index].makeCode("].concat("), concatPart, (last list).makeCode(")")
+#### Expansion
+
+# Used to skip values inside an array destructuring (pattern matching) or
+# parameter list.
+exports.Expansion = class Expansion extends Base
+
+ isComplex: NO
+
+ compileNode: (o) ->
+ @error 'Expansion must be used inside a destructuring assignment or parameter list'
+
+ asReference: (o) ->
+ this
+
+ eachName: (iterator) ->
+
#### While
# A while loop, the only sort of low-level loop exposed by CoffeeScript. From
View
16 test/assignment.coffee
@@ -268,6 +268,22 @@ test "#2055: destructuring assignment with `new`", ->
{length} = new Array
eq 0, length
+test "#156: destructuring with expansion", ->
+ array = [1..5]
+ [first, ..., last] = array
+ eq 1, first
+ eq 5, last
+ [..., lastButOne, last] = array
+ eq 4, lastButOne
+ eq 5, last
+ [first, second, ..., last] = array
+ eq 2, second
+ [..., last] = 'strings as well -> x'
+ eq 'x', last
+ throws (-> CoffeeScript.compile "[1, ..., 3]"), null, "prohibit expansion outside of assignment"
+ throws (-> CoffeeScript.compile "[..., a, b...] = c"), null, "prohibit expansion and a splat"
+ throws (-> CoffeeScript.compile "[...] = c"), null, "prohibit lone expansion"
+
# Existential Assignment
View
15 test/functions.coffee
@@ -178,6 +178,21 @@ test "default values with splatted arguments", ->
eq 1, withSplats(1,1,1)
eq 2, withSplats(1,1,1,1)
+test "#156: parameter lists with expansion", ->
+ expandArguments = (first, ..., lastButOne, last) ->
+ eq 1, first
+ eq 4, lastButOne
+ last
+ eq 5, expandArguments 1, 2, 3, 4, 5
+
+ throws (-> CoffeeScript.compile "(..., a, b...) ->"), null, "prohibit expansion and a splat"
+ throws (-> CoffeeScript.compile "(...) ->"), null, "prohibit lone expansion"
+
+test "#156: parameter lists with expansion in array destructuring", ->
+ expandArray = (..., [..., last]) ->
+ last
+ eq 3, expandArray 1, 2, 3, [1, 2, 3]
+
test "default values with function calls", ->
doesNotThrow -> CoffeeScript.compile "(x = f()) ->"
Something went wrong with that request. Please try again.