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

Destructuring assignments #4300

Closed
Simn opened this issue Jun 8, 2015 · 22 comments
Closed

Destructuring assignments #4300

Simn opened this issue Jun 8, 2015 · 22 comments

Comments

@Simn
Copy link
Member

Simn commented Jun 8, 2015

Could we allow this:

var obj = { foo: 12, bar: [13] };
{ foo: x, bar: [y] } = obj;
trace(x);
trace(y);

by transforming it to this:

var obj = { foo: 12, bar: [13] };
var x;
var y;
switch (obj) {
    case { foo: 12, bar: [13] }:
        x = 12;
        y = 13;
    default:
        throw "Match failure";
}
trace(x);
trace(y);

?

This does not require any new syntax and can be detected by checking if the left-hand argument of an assignment binop is something that at the moment gives an invalid assignment error anyway. The implementation itself is straightforward, too.

Use-case: I often have to extract multiple values like so:

[cName, aName] = switch (Context.getLocalType()) {
    case TInst(c, [TAbstract(a, tl)]): [c.toString(), a.toString()];
    case t: Context.error("Unexpected type", p);
}

Doing that by hand is verbose and error-prone.

@nadako
Copy link
Member

nadako commented Jun 8, 2015

A HUGE +1 on this! Destructuring is so sweet, however I wonder if this syntax is good. We introduce new locals without the var keyword which seems inconsistent. Maybe we should do var {foo: x} = obj;. Also, I believe @pleclech implemented that in his fork, I wonder what syntax did he use.

@back2dos
Copy link
Member

back2dos commented Jun 8, 2015

Instead of this:

{ foo: x, bar: [y] } = obj;
trace(x);
trace(y);

You can write this already:

switch obj {
  case { foo: x, bar: [y] }:
    trace(x);
    trace(y);
}

It's not like it's so much worse, is it? I'm just pointing it out, because it currently seems impossible to generally define make supported syntax without presently defined semantics to do something useful. Is that policy open for debate now?

Regarding the issue at hand, I've had a hard time adding this to tink_lang, because I found I had two choices, none of which I like:

  1. Create a default branch that throws an exception. That's a weird thing to do in a language as Haxe, where the compiler can actually tell you whether you're being exhaustive.
  2. Just generate one case statement. This works pretty well, but the error message is really confusing if you don't know what's going on underneath.

Let's look at the second example you gave. I suppose you'd write that like so?

TInst(_.toString() => cName, [TAbstract(_.toString() => aName, _)]) = Context.getLocalType();

Aside from the question whether this is really readable and the issue of exhaustiveness (let's just assume there's a solution), there's another issue: How do you know this is a destructuring assignment? Anything that is an ECall could just be a macro call that actually produces a valid lhs expression (or the return type might be an abstract for which we could eventually support defining = itself). So you need to type the callee to see if it's an enum constructor and you'll have to type it against the rhs (to be able to resolve non-imported or conflicting constructor names (e.g. TAnonymous)), which makes this syntax become very context sensitive.

In my attempts I've used @var which I think is explicit and self-explanatory. Of course just being able to write var $e{pattern} = $e{value} would be great (I've seen that on some experimental fork). If you're concerned with having to change the AST, I think it could still just be represented as EMeta instead, for the time being.

If you can get this right, I'm all for it. I just wanted to warn you of all the issues I had trouble solving. Oh and I'm also slightly afraid that this might backfire to fuel the short lambda flame war, but I'll leave the politics to you guys ;)

@pleclech
Copy link

pleclech commented Jun 8, 2015

Le 08/06/2015 13:18, Dan Korostelev a écrit :

A HUGE +1 on this! Destructuring is so sweet, however I wonder if this
syntax is good. We introduce new locals without the |var| keyword
which seems inconsistent. Maybe we should do |var {foo: x} = obj;|.
Also, I believe @pleclech https://github.com/pleclech implemented
that in his fork, I wonder what syntax did he use.


Reply to this email directly or view it on GitHub
#4300 (comment).

I tried to use es6 syntax :
http://hacking-haxe.atouchofcode.com/#b7889

I forget to add the case when you want to reuse already existant var.

In my opinion you should let the choice to the user to declare new var or reuse old one.
Patrick.

@ousado
Copy link
Contributor

ousado commented Jun 8, 2015

I like the original proposal. The assignment makes clear enough that bindings are being introduced, and it's concise.

@pleclech
Copy link

pleclech commented Jun 8, 2015

By the way i am not using a switch but doing a direct assignment field per field while destructuring, i m doing this early and let the compiler complain if the field did not exist.

@frabbit
Copy link
Member

frabbit commented Jun 8, 2015

@pleclech can you destructure enums in your implementation?

@pleclech
Copy link

pleclech commented Jun 8, 2015

Le 08/06/2015 14:37, frabbit a écrit :

@pleclech https://github.com/pleclech can you destructure enums in
your implementation?


Reply to this email directly or view it on GitHub
#4300 (comment).

I think not.

@pleclech
Copy link

pleclech commented Jun 8, 2015

Le 08/06/2015 14:37, frabbit a écrit :

@pleclech https://github.com/pleclech can you destructure enums in
your implementation?


Reply to this email directly or view it on GitHub
#4300 (comment).

What will be the syntax ?
I think in that case you fall more into the switch case syntax ?

@back2dos
Copy link
Member

back2dos commented Jun 8, 2015

If you restrict this to object literals only, then yes, it is unambiguous.
But does that restriction honor the principle of the least surprise? And
how useful is it then anyway?

Most of all, the assumption about this being unambiguous is short sighted.
There's a proposal for adding operator overloading through static
extensions and the idea of being able to redefine = is also around for a
while now. We can't have this proposal and the two other, since then { x:x } = xyz might become ambiguous. So it's not something that can be added
without affecting the language otherwise.

It is also the least bit clear how the second example is to work. Through
arrays or actually through pseudo-tuples (i.e. like switch [a, b, c]
does)? The latter would be a great way to sidestep exhaustiveness issues
and to extract mixed types and what not, but it's not just a matter of just
rearranging stuff into a switch statement.

On Mon, Jun 8, 2015 at 1:49 PM, ousado notifications@github.com wrote:

I like the original proposal. The assignment makes clear enough that
bindings are being introduced, and it's concise.


Reply to this email directly or view it on GitHub
#4300 (comment)
.

@ncannasse
Copy link
Member

ncannasse commented Jun 8, 2015

While I agree it can be useful in some cases, it does not weight very well in terms of readability and error proneness. It also requires introducing new syntax such as var [a,b] = foo(), {x:x,y,y}=bar(), since most of the time you want to destructure in local vars, which will require IDE support for it, etc etc etc

Regarding the problem you're trying to solve, what's wrong with the following:

var names = switch (Context.getLocalType()) {
    case TInst(c, [TAbstract(a, tl)]): { c : c.toString(), a : a.toString() };
    case t: Context.error("Unexpected type", p);
}
//...
trace(names.c)
trace(names.a)

I think that's a good enough solution.
Let's keep Haxe simple, please :)

@Simn Simn closed this as completed Jun 8, 2015
@nadako
Copy link
Member

nadako commented Apr 12, 2016

Note that many modern and widely accepted languages have/are getting destrcturing assignments now in one way or another. Just to name a few that come to mind:

  • ES6
  • TypeScript
  • CoffeeScript
  • C# 7 (tuples)
  • Python (tuples)

It's not like it's something from the "functional world" and it'll be more popular with time thanks to js and its transpilers.

@fponticelli
Copy link
Contributor

I like how the debate starts productive and good ideas are laid down and
then it is suddenly and irrevocably shut down. The argument of keeping Haxe
simple is gone for a long time now and it is time for the language to
evolve. As a user I feel more and more frustrated by the absence of some
features and the lack of the will to implement them.

On Tue, Apr 12, 2016 at 11:05 AM, Dan Korostelev notifications@github.com
wrote:

Note that many modern and widely accepted languages have/are getting
destrcturing assignments now in one way or another. Just to name a few that
come to mind:

  • ES6
  • TypeScript
  • CoffeeScript
  • C# 7 (tuples)
  • Python (tuples)

It's not like it's something from the "functional world" and it'll be more
popular with time thanks to js and its transpilers.


You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub
#4300 (comment)

@nadako
Copy link
Member

nadako commented Apr 12, 2016

Well, I think Nicolas was talking about keeping the syntax simple and I can agree with this intent. I love the simplicity and even minimalism of Haxe syntax, but pragmatically I often feel the need for tuples+destructuring, so I'm still very much in favor of this feature.

@fponticelli
Copy link
Contributor

I bet he is, but we could have discussed how to make this feature in a way
that is simple to understand instead of shutting it down, no? If we can't
(don't see why not) it is a very legitimate thing to give up on a feature.
I think Haxe seriously needs some A/B testing ... just put some of those
new features out in the dev branch and collect user feedback, then make a
decision. Guessing and limiting it is not the way to go in my opinion.

On Tue, Apr 12, 2016 at 12:22 PM, Dan Korostelev notifications@github.com
wrote:

Well, I think Nicolas was talking about keeping the syntax simple and I
can agree with this intent. I love the simplicity and even minimalism of
Haxe syntax, but pragmatically I often feel the need for
tuples+destructuring, so I'm still very much in favor of this feature.


You are receiving this because you commented.
Reply to this email directly or view it on GitHub
#4300 (comment)

@nadako
Copy link
Member

nadako commented Apr 12, 2016

I agree, maybe we need to develop some enhancement proposal process, like Python's PEP or something.

@jdonaldson
Copy link
Member

jdonaldson commented Apr 15, 2016

Add lua to the list of multiple-returns/tuple languages.

I might add something: Supporting simple multiple returns is more pressing than a full blown destructuring feature imho. It would allow for more natural extern support on the languages that use them in core libraries. It's currently impossible to get Haxe to deal with multivalue return functions in a first class way. You either have to wrap the result somehow (which adds overhead), only type/provide the first argument (which ends up losing info on the other values returned), or use magic methods (and give up communicating anything about the type).

I have a branch where I'm poking around on a more elegant solution. The way I have in mind is to add some metadata to a standard typedef:

@:MultiReturn typedef Foo = {
   a : Int;
   b : Int;
}
...

var f : Foo = some_extern() // extern function that returns multiple values 1, 2
trace(f.a); // 1
trace(f.b); // 2

Whenever that type is used in assignment, the compiler detects it and does platform-specific logic for handling multiple returns.

local _hx_1, _hx_2 = some_extern()
print(_hx_1)
print(_hx_2)

This seems to be straightforward, but the first problem is that anonymous fields (used for typedefs) are a map, so they don't preserve ordering in the compiler metadata that is necessary for preserving the tuple positions. My workaround there was to leverage the cf_pos info to capture the source columns and lines they're declared at, and sort them separately that way when necessary.

The second problem is that the MultiReturn variable needs to maintain a local variable alias for each of its fields, which it will use for assignment and access. That's not so bad though, it just needs to be stored somewhere in the data the compiler uses to track variables.

So, the steps would include: detect the @:MultiReturn metadata on the types during assignment, and then in a TVar declaration stash a new temp variable name in the a_fields. Later in the TVar declaration print out the temp variable names in cf_pos order. During field access, use its temp variable name instead.

Also, I suppose rather than using metadata, we could use a special extern class like haxe.extern.MultiReturn<T> which would wrap a normal typedef.

This is completely half baked at this point, so any thoughts are welcome.

@back2dos back2dos mentioned this issue Apr 15, 2016
@fullofcaffeine
Copy link

Since we are now getting short-lambdas soon, could we reopen/reconsider this as well?

@frabbit
Copy link
Member

frabbit commented Dec 14, 2016

i like destructuring, but this has to go through haxe-evolution with a well thought out proposal which covers all the corner cases and limits of destructuring.

@fullofcaffeine
Copy link

Let's do it then! @Simn since you originally proposed it, would you be able to do that?

As a related question - are there any ways to simulate this via a macro as of now? Are there any existing solutions for that?

@Simn
Copy link
Member Author

Simn commented Dec 15, 2016

I want to focus on stability for a while so I'm not looking for any new features to implement right now. To be honest I'd be reluctant to vote for my own feature suggestions at the moment.

@sebthom
Copy link
Contributor

sebthom commented May 15, 2017

In case anyone finds this useful, I wrote a macro as part of the haxe-strings library, which implements assignment destructuring for array-accessible objects, see /vegardit/haxe-strings/blob/master/src/hx/strings/internal/Macros.hx#L143

Macro.unpack([a,b,c] = ["1", "2", "3"]);
trace(a);

// or
var list = ["1", "2", "3"];
Macro.unpack([a,b,c] = list);
trace(a);

// or
Macro.unpack([prefix,value] = "foo:bar".split(":"));
trace(prefix);

@vonagam
Copy link
Member

vonagam commented Sep 28, 2020

@Simn, almost 4 years have passed since you said that it is not right time to create a proposal and wanted to focus on stability. Is it still not the time? (I created the alternative syntax proposal, but just to start a discussion, i do prefer to have the same syntax as other languages if possible, as described here.)

@ncannasse (sorry to bother), if a theoretical proposal (with the design described here) is created and passes an evolution vote will you veto it on grounds of not adhering to your vision for haxe? (As i understand this concern is the only thing that keeps a proposal from being introduced.)

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

No branches or pull requests