Skip to content

Proposal: Syntactic sugar: Sub-structuring #4991

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

Closed
aminland opened this issue Feb 25, 2018 · 20 comments
Closed

Proposal: Syntactic sugar: Sub-structuring #4991

aminland opened this issue Feb 25, 2018 · 20 comments

Comments

@aminland
Copy link

aminland commented Feb 25, 2018

Note that some prior discussion on this was started in #4919, before that thread turned into a discussion on something unrelated.

Right now, destructuring is only supported as a means to assign properties to vars in top level scope.
The proposal is for a new syntax to support assigning properties from one object into another object (i.e. building / extending objects). I've borrowed the term "substructuring" from livescript.

The proposed sugar would behave as follows (js output shown):

# rhs substructuring (basically _.pick)
y = data{a, b, c}    
# >>    y = ({a:_a, b:_b, c:_c} = data, {a:_a, b:_b, c:_c})
# lhs substructuring
x{a, b, c} = data    
# >>    ({a: x.a, b: x.b, c: x.c} = data);
# or for easier implementation, reuse rhs output:
# >>    x = _extends(x, ({_a, _b, _c} = data, {a:_a, b:_b, c:_c}))

I think this would make a lot of things simpler in coffeescript, as the syntax is much more concise. The important thing to note here is that there must not be a whitespace between the source object and the { symbol, allowing easy differentiation between this syntax and a function call.

Thoughts?

@aminland
Copy link
Author

aminland commented Feb 25, 2018

Just following up on the thought process... If we fully implement sub-structuring, following should also work:

user{title, name:full_name} = data 
# >>    ({title: user.title, name: user.full_name} = data);
user{addr:{street:[street1, street2], city, state, zip} } = data    
# >>    ({addr:{street:[user.street1, user.street2], city:user.city, state:user.state, zip:user.zip} } = data);
book = data{title, authors:[{name:author},{name:coauthor=null}]}
# >>     book = ({title:_title, authors:[{name:_name1},{name:_name2=null}]} = data, {title:_title, author:_name1, coauthor:_name2});

We can optionally support rest/spread syntax, with an easily understandable way to subtract unwanted props:

user{name, birthdate, -password, ...extra_info} = data 
# >>    ({name: user.name, password:_blackhole, ...user.extra_info} = data);
record = data{id, -unwanted, ...properties}
# >>    record = ({id:_id, unwanted:_blackhole, ..._properties} = data, {id:_id, properties: _properties});

Sub-structuring doesn't have to used in an assignment:

console.log data{id, -unwanted, ...properties}
# >>    console.log({id:_id, unwanted:_blackhole, ..._properties} = data, {id:_id, properties: _properties});

"Re-structuring" (another made up term, super nice to have, but possibly overkill):

users.push data{id, name:fullname, {[str1, str2]:=street, city, state, zip:postal}:=address}
# >>    users.push((
# >>        {id:_id, name:_name, str1:_str1, str2:_str2, city:_city, state:_state, zip:_zip} = data, 
# >>        {id:_id, fullname:_name, address: {street:[_str1, _str2], city:_city, state:_state, postal:_zip }}
# >>    ));

data{a, {b:{c, [x,y]:=z}}:=d}
# >>   ({a:_a, b:{c:_c, x:_x, y:_y}} = data, {a:_a, d:{c:_c, z:[_x, _y]}});

@vendethiel
Copy link
Collaborator

See also #1617 / #1708

@GeoffreyBooth
Copy link
Collaborator

Can you please explain this with more detailed examples?

user = undefined

data =
  title: 'Mr'
  surname: 'Coffee'

user{ title, name: surname } = data

What does user equal now? Or is the point to declare new title or name variables? I’m not following your proposal.

@GeoffreyBooth GeoffreyBooth changed the title Syntactic sugar proposal: Sub-structuring Proposal: Syntactic sugar: Sub-structuring Feb 26, 2018
@aminland
Copy link
Author

aminland commented Feb 26, 2018

Sorry if I missed a few things in the descriptions. In your example the point would be to set title and surname properties on user.
In general, for the lhs synax, it is only meant as a way to update an existing object, so we would not be declaring any new variables. This would result in a runtime exception, similar to how you would get an exception today if you just wrote user.name = data.surname if user was undefined.

We could also add a compile-time check similar to the current error thrown if you write user ?= {} without first defining user, but it shouldn't be needed since we would also have the rhs syntax. If a user would be looking to create a new object, they should use the rhs syntax.

@zdenko
Copy link
Collaborator

zdenko commented Feb 27, 2018

@aminland I'm working on a PR for this and I want to check if I'm on the right track.
So far I got this working:

data =
  title: "Mr"
  name: "Coffee"
  address: "Sesame Street"

view = data{title, name:full_name}
# view = { title: "Mr", full_name: "Coffee" }

view = data{title, name:full_name, rest...}
###
view = {
  title: "Mr", 
  full_name: "Coffee",
  rest: {
    address: "Sesame Street"
  }
}
###

console.log  data{title, name:full_name}
#  { title: "Mr", full_name: "Coffee" }

data =
  title: "Mr"
  name: "Coffee"
  address: "Sesame Street"

view{title, name:full_name} = data
###
error: the variable 'view' has not been declared before
view{title, name:full_name} = data
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
###

data =
  title: "Mr"
  name: "Coffee"
  address: "Sesame Street"

view = address: "Elm Street"
view{title, name:full_name} = data
###
view = { address: 'Elm Street', title: 'Mr', full_name: 'Coffee' }
###

@connec
Copy link
Collaborator

connec commented Feb 27, 2018

I am all for this 👍 Especially the rhs substructuring which would make it possible to neatly deal with data in an immutable/declarative way and eliminate many an intermediate variable when wrangling objects between interfaces.

I'm less sure about the -key exclusion. I feel like that could be treated separately since that could also be added to the existing destructuring assignment.

{ a, -b, c... } = d

Syntax-wise I'm not sure if I prefer obj{key} as discussed here or obj.{key} as discussed in older issues.

@zdenko
Copy link
Collaborator

zdenko commented Feb 27, 2018

Just a quick update on the progress.
Exclusion key (-key) is added:

data =
  title: "Mr"
  name: "Coffee"
  address: "Sesame Street"
  zip: 10001

view = city: "London"
view{title, name, -zip, rest...} = data
# view = {city: "London", title: "Mr", name: "Coffee", rest: {address: "Sesame Street"}}

@connec your example also compiles.

d = {a, b, x, y}
{a, -b, c...} = d
###
a = a
c = {x, y}
###

I have to clean up the code a little bit and then I'll create a PR.

@jashkenas
Copy link
Owner

Sorry to dampen the mood, folks, but we should not implement this feature. Being whitespace sensitive to this degree is confusing, and more harmful than the feature is beneficial. As you all know quite well:

circle.move {x, y}

Expands into:

circle.move({x: x, y: y});

If

circle.move{x, y}

Became something entirely different, that would be a big step backwards in readability for CoffeeScript.

@aminland
Copy link
Author

aminland commented Feb 27, 2018

@jashkenas but we already have this type of sensitivity for whitespace:
point [x] is very different than point[x].

The whole greatness of CoffeeScript stems exactly from this whitespace sensitivity, no?

@jashkenas
Copy link
Owner

@jashkenas but we already have this type of sensitivity for whitespace:
point [x] is very different than point[x].

And it's not ideal there either!

@vendethiel
Copy link
Collaborator

vendethiel commented Feb 27, 2018

We have the sensitivity everywhere. f +1 vs f + 1, a ? b vs a? b, the case pointed out here, etc. I support this.

@jashkenas
Copy link
Owner

Alright, then, I'm overruled. If y'all are all for it, then go for it!

@connec
Copy link
Collaborator

connec commented Feb 27, 2018

point [x] is very different than point[x].

And it's not ideal there either!

Could make them both require . - at the cost of one hell of a breaking change 😄

point.{x, y}
point.[x]

@aminland
Copy link
Author

aminland commented Feb 28, 2018

require . - at the cost of one hell of a breaking change

Please no breaking changes 😭

I had suggested point{x} for consistency with array access and array assignment (and also i figured it would be easier to parse the grammar for it, since you wouldn't have to special case the existing . attribute access).

@carlsmith
Copy link
Contributor

The distinction between x[y] and x [y] is not equivalent to the proposed distinction between x{y} and x {y}, as the existing distinction is based on well established conventions.

As well, why would using the dot (as in point.{x, y}) make point[x] inconsistent? They do two unrelated things. In a language like Python, where x[y] operates on dictionary keys, having x{y} to access the object's namespace would be cool, and it would make sense that the two are syntactically consistent. I don't get the reasoning here though??

Reserving the proposed grammar just seems prudent, and point.{x, y} is ideal for this anyway IMO.

@GeoffreyBooth
Copy link
Collaborator

I’m worried about getting ahead of the ECMA committee on this. We dodged a bullet in that the meaning of ES2015 destructuring syntax was essentially identical to how we had defined it, thanks in no small part to the fact that we had pioneered it, of course. But we weren’t so lucky when it came to other things like how they redefined function default values, which led to one of the CS2 breaking changes. If we incorporate this feature and declare that this syntax means this functionality, what happens if or when ECMA decides to use this syntax for something else? Or for predominantly the same thing, but with differences?

I understand that the logical conclusion of my argument means that CoffeeScript cedes its role as a contributor of innovation in JavaScript. That saddens me, and I’d like to avoid it if we can find a way to thread this needle.

But JavaScript is not moribund anymore, like it was in the days when CoffeeScript flourished. There is a process for proposing new JavaScript features and getting them accepted: the TC39 committee. Perhaps this should be proposed there, as a potential new syntax and feature for JavaScript. If it reaches Stage 3, we can merge this PR (with its polyfill implementation) into CoffeeScript. When the syntax reaches Stage 4, we can refactor this so that CoffeeScript outputs the syntax “as is.” By working with the committee, we’ll ensure that whatever ends up incorporated into JavaScript doesn’t cause breaking changes in CoffeeScript, and we’ll improve JavaScript too.

@Inve1951
Copy link
Contributor

Inve1951 commented May 4, 2018

note that this is already possible (sort of) like this:

-> {a: @aa, @rest...} = obj2
.call obj1

try it.

@osddeitf
Copy link

osddeitf commented May 8, 2018

Why not obj{.a, .b, .c}, it'll compile to { obj.a, obj.b, obj.c }, i've seen it in "neo4j". It'll no matter with spaces, and can also help to combine with other field, like: obj{ .a, .b, .c, msg: 'Hello world' }

@GeoffreyBooth
Copy link
Collaborator

Closing per #4991 (comment).

@GeoffreyBooth
Copy link
Collaborator

There appears to be a similar proposal under discussion on esdiscuss: https://esdiscuss.org/topic/extended-dot-notation-pick-notation-proposal

I wouldn't be surprised if some proposal for picking elements from objects doesn't advance soon.

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

9 participants