DEP: Destructuring

114 views
Skip to first unread message

Kenneth Endfinger

unread,
Mar 27, 2015, 8:38:31 PM3/27/15
to core...@dartlang.org
This is the main thread to discuss the DEP for Destructuring.

Paul Brauner

unread,
Mar 31, 2015, 12:19:46 PM3/31/15
to Kenneth Endfinger, core...@dartlang.org
Hi Kenneth,

I have no authority whatsoever regarding Dart, the following is just my personal opinion.

I'm hoping that one day Dart can have full-blown pattern matching. I know this is not going to happen anytime soon so I'm all for having a lightweight version first like the one you propose. I think it would be nice if we could keep this in mind while discussing your DEP though: we should try to make destructuring forward-compatible with a possible future implementation of pattern matching in Dart.

I know this sounds impossible as there is no spec for that yet, but we can at least take some guesses. For instance it seems to me that staying away as much as possible from ad-hoc syntax for certain datatypes is a good start (although there's already a special syntax for constructing lists and maps, so it sounds ok to have a special syntax to deconstruct them too). Also we should try to ask yourselves whether the many notations you introduce are somehow consistent with respect to scoping.

Cheers,
Paul

PS: I'm not familiar with the way DEP are discussed. Is this the right place?

On Sat, Mar 28, 2015 at 1:38 AM Kenneth Endfinger <kaend...@gmail.com> wrote:
This is the main thread to discuss the DEP for Destructuring.

--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+u...@dartlang.org.

Bob Nystrom

unread,
Mar 31, 2015, 12:47:00 PM3/31/15
to Paul Brauner, Kenneth Endfinger, core...@dartlang.org

On Tue, Mar 31, 2015 at 12:37 AM, 'Paul Brauner' via Dart Core Development <core...@dartlang.org> wrote:
PS: I'm not familiar with the way DEP are discussed. Is this the right place?

Totally. If you have more targeted feedback, you can also file issues on the repo for the DEP itself. But here is a good place to generally talk about it.

- bob

Bob Nystrom

unread,
Mar 31, 2015, 12:53:39 PM3/31/15
to Paul Brauner, Kenneth Endfinger, core...@dartlang.org
I think this proposal is a great start. ES6 has destructuring binding and I think Dart will fall behind in expressiveness if we don't consider something similar.

What will be tricky is figuring out how it works with Dart's current grammar. Unfortunately, the declaration syntax our language team chose for types doesn't make this easy. I'll start with—in theory—the simplest part of the proposal, destructuring lists:

var [a, b] = [1, 2];

This is fine, and would work fine in JS too. This also looks reasonable:

var [String a, int b] = ["s", 2];

But Dart's declaration syntax also allows using a type name to create a variable:

String a = "s";

So how does that interact with this proposal? Would you allow something like:

List<String> [a, b] = ["s", "t"];

How about:

List [String a, int b] = ["s", 2];

We could restrict this to declarations that use var or final, but some people may think that makes the C-style declaration syntax a second-class citizen in Dart.

I have feedback about other parts of the proposal, but let's start here. :)

Thoughts?

- bob

Paul Brauner

unread,
Apr 1, 2015, 6:03:15 AM4/1/15
to Bob Nystrom, Kenneth Endfinger, core...@dartlang.org
On Tue, Mar 31, 2015 at 6:53 PM Bob Nystrom <rnys...@google.com> wrote:
I think this proposal is a great start. ES6 has destructuring binding and I think Dart will fall behind in expressiveness if we don't consider something similar.

What will be tricky is figuring out how it works with Dart's current grammar. Unfortunately, the declaration syntax our language team chose for types doesn't make this easy. I'll start with—in theory—the simplest part of the proposal, destructuring lists:

var [a, b] = [1, 2];


This already looks weird to me: what if I want b to be final but not a?

  [var a, var b] = [1, 2];

makes it look more intuitive to me.
 
This is fine, and would work fine in JS too. This also looks reasonable:

var [String a, int b] = ["s", 2];


Following my "proposal" above, this would just be:

  [String a, int b] = ["s", 2];
 
But Dart's declaration syntax also allows using a type name to create a variable:

String a = "s";

So how does that interact with this proposal? Would you allow something like:

List<String> [a, b] = ["s", "t"];
 

And this would just not make sense. I'm not sure what the "List" par of "List<String>" is useful for here: no variable of type List is introduced in the scope, so it seems useless to me. Or is the intent that "List<String>" should match the type of the right-hand side (and warn/fail in checked mode otherwise)?
 
How about:

List [String a, int b] = ["s", 2];


Same remark here.

Asger Feldthaus

unread,
Apr 1, 2015, 11:49:36 AM4/1/15
to core...@dartlang.org
Overall I like the idea, but I have some concerns regarding "usage with access":

target.[a, b] = [1, 2];

Could you please clarify what this means?

Does it mean this?

target[a] = 1;
target
[b] = 2;

I think it also reads a lot like this:

target.a = 1;
target
.b = 2;

Or maybe:

var tmp = [1, 2];
target
[a] = tmp[a];
target
[b] = tmp[b];


We should consider how this interacts with the cascade operator. With the above syntax we now have three similar expressions:

target[x]   = foo;
target
.[x]  = foo;
target
..[x] = foo;

The first and last one both assign foo into target[x]. The one in the middle, on the other hand, assigns foo[0] into target[x] (or target.x, depending on the above).
This is somewhat counterintuitive, and there is no way to add a cascading variant of middle one without a triple dot operator.

I also feel that this particular construct is solving a problem that the cascade operator already provides a solution for:

target.[a,b] = [1, 2]

is the same as:

target ..[a] = 1 ..[b] = 2;

The cascade is not as pretty and doesn't work when the right-hand side is not a literal. But when the right-and side is not a literal, I have a hard time seeing what actually happens.

Kenneth Endfinger

unread,
Apr 1, 2015, 5:03:34 PM4/1/15
to Paul Brauner, Bob Nystrom, core...@dartlang.org
I'm going to make an effort to change the syntax. I quite like the idea of:

    {var a, var b} = [5, 5];

But:

   var {a, b} = [5, 5];

this should also be supported. Perhaps if it is on the far left, it should be applied to all, and if it's specified for each identifier, it applies to only that one. 

Kenneth Endfinger

unread,
Apr 1, 2015, 5:04:28 PM4/1/15
to Asger Feldthaus, core...@dartlang.org
This would mean:

target.a = 1;
target.b = 2;

I would like to make this better but I don't see a good way to do it. I will take suggestions (as always) on what to do to fix this.

--

Paul Brauner

unread,
Apr 2, 2015, 4:37:04 AM4/2/15
to Kenneth Endfinger, Bob Nystrom, core...@dartlang.org
On Wed, Apr 1, 2015 at 11:03 PM Kenneth Endfinger <kaend...@gmail.com> wrote:
I'm going to make an effort to change the syntax. I quite like the idea of:

    {var a, var b} = [5, 5];

But:

   var {a, b} = [5, 5];

this should also be supported. Perhaps if it is on the far left, it should be applied to all, and if it's specified for each identifier, it applies to only that one. 


I would beware of syntactic sugar like this. My advice would be to get some solid proposal without sugar and then add it if examples scream for it.

Dan Schultz

unread,
Apr 2, 2015, 3:55:01 PM4/2/15
to core...@dartlang.org
What about destructuring for named arguments on functions and constructors? It'd be useful for creating instances from JSON objects, or making it easier to write immutable classes. Although, having something like Elm's records would be ideal for immutable objects.

Dan

Bob Nystrom

unread,
Apr 2, 2015, 4:04:03 PM4/2/15
to Paul Brauner, Kenneth Endfinger, core...@dartlang.org

On Wed, Apr 1, 2015 at 3:03 AM, Paul Brauner <po...@google.com> wrote:
This already looks weird to me: what if I want b to be final but not a?

  [var a, var b] = [1, 2];

I would be perfectly happy not supporting that use case. :)

I think it's a fine restriction to say that all of the variables declared using a destructuring have the same final-ness.

- bob

Paul Brauner

unread,
Apr 3, 2015, 5:27:20 AM4/3/15
to Bob Nystrom, Kenneth Endfinger, core...@dartlang.org

Ok, maybe my motivation wasn't a good one. I think what made me react is that it's incompatible with declaring the types of individual variables (at least if we want the syntax to be consistent with Dart's current syntax where var goes where a type annotation would go or vice-versa).

Erik Ernst

unread,
Apr 4, 2015, 7:09:31 AM4/4/15
to Paul Brauner, Bob Nystrom, Kenneth Endfinger, core...@dartlang.org
Hi all,

It might be useful to try to retrace the fundamental structure of pattern matching related language constructs. I think the core is the following two-step process:
  1. Specify a situation, including a choice of named locations in that situation.
  2. Check whether that situation is present, and if so: bind values to names accordingly.
We may then run code where those names are in scope.

An SML function definition is a typical case.  Here, step 1 occurs at compile time and step 2 at runtime.  We'd expect help from the compiler checking that we have lined up a complete (but possible somewhat redundant) set of alternative situation specifications, such that it is guaranteed that execution will not be stuck at runtime. We will choose the (textually) first specification to which the given runtime situation conforms, bind the names to values accordingly, and run the associated code:

  fun sum [] = 0
      | sum [x] = x  (* redundant, but not unused *)
      | sum (x::xs) = x + sum xs;

With algebraic datatypes, lack of subtyping, and names that stand for a value rather than standing for a storage location (i.e., immutable rather than mutable variables), we can use inference to find the best possible typing for each name, so there is rarely a need to declare the types of these names. This is a quite wholesome design.  But we cannot use it directly, because we have mutability, subtyping, and OO encapsulation.

Views (see the paper mentioned by Paul) will allow us to create an arbitrary isomorphism between a representation type and a 'view type' (so if we want to view a primitive int value as a Peano 'Zero | Succ n' style inductive type, we can define functions to take us from one to the other, unambiguously).  The property that such a pair of functions is _actually_ a pair of exactly inverse functions is left unchecked, the programmer of such functions must be careful, and everybody else must trust or check that it is correct.

In the slightly more general setting that I've outlined with the steps 1 and 2 above, views allow us to specify situations in such a way that an arbitrary (encapsulated) representation can be tested as being isomorphic (or not) to a given specification, and in that case also which values we find in specific locations inside the view type value, such that we can bind names to values.

But I have the impression that views require strict static typing, because it relies crucially on compilers adding applications of the isomorphism (mapping values from the view type to the representation type, and vice versa) at locations where the view type is expected and the representation type is encountered, and vice versa.

That just doesn't work for Dart, where it is a core value that programs must be able to run independently of any type annotations (and, in particular, we cannot require that type annotations are present or even consistent).  NB:  This is a feature, not a bug!  ;-D

The other thing that doesn't work out of the box is the choice to bind _values_ to names in the view type domain.  With mutable state, we'd get a weird, half-baked type of access to the actual entities in question when we can just read their values at the beginning of a scope: We need access that corresponds to the nature of the underlying entities, e.g., such that we can mutate a getter/setter pair (say, a field).

Hence, to me, the obvious mechanism corresponding to functional style pattern matching for an OO language with no requirement for strict static typing is a wrapping mechanism:
  1. Specify an excerpt of an object graph and a 'wrapper' class that provides an interface for us to work with the objects in that object graph
  2. Check whether a "matching" object graph exists, and if so create and initialize a wrapper object for it.

Applying that idea to the given deconstruction proposal is a separate (presumably long and detailed) discussion, but one simple idea could be as follows:

A variant of list literals may be used to specify an object graph consisting of a list and a number of elements in that list, and we can specify properties of each element using declaration syntax:

  var [a, b] = myExpressionExpectedToDeliverAList;
  // corresponds to
  List freshName = myExpressionExpectedToDeliverAList;
  assert(freshName.length == 2);
  var a = freshName[0];
  var b = freshName[1];

  var <num>[int a, double b, ...] = myExpression;
  // corresponds to
  List<num> freshName = myExpression;
  int a = freshName[0];
  double b = freshName[1];

For user-defined classes (where no special literals are present in the syntax), we could use constructor calls with declarations as arguments:

  var Pair(int x, int y) = someExpression;
  // corresponds to
  Pair freshName = someExpression;
  int x = freshName.computeConstructorArgOneFor_int_int;
  int y = freshName.computeConstructorArgTwoFor_int_int;

We would need to come up with a sufficiently useful and flexible way to declare how to compute the "pattern matching constructor arguments".  Presumably the class Point would have to be written in such a way that it passes a certain compile time check a la "this class knows how to match an (int, int) constructor argument list", or maybe just ".. a constructor argument list of length two" (adjust the name to "computeConstructorArg...ForTwoArgs"), and the corresponding details of how to generate code for "computeConstructorArg..." would be well-defined when the class passes that check.

This is a quite restricted idea because it only allows us to specify a situation (a partial object graph) which is constituted by a single object (a List, a Pair, etc.), but it does let us specify a view-like relation (it is encapsulated in the Pair class how to obtain a suitable 'x' and 'y' from a given Pair).

With a more general model, we could specify object graphs like "we must have a Tree, call it t, and there must be a path along 'left' and 'right' references to another Tree, t2, such that t2.value == 142", and then initialize 'Tree t' and 'Tree t2' in case that situation exists.  When that situation does not exist, it would be nice to be able to specify an "else" specification, rather than crashing with a "pattern match failed", but that shouldn't be so hard.  Ah, let's add a wild guess at some syntax, too:

  if (Tree t where t2 == t.[left|right]* && t2.value == 142 = myTreeExpression) {
    // Use t and t2.
  } else {
    Tree t = myTreeExpression;
    // Use t, possibly exploiting the knowledge that it does not contain 142
  }

Don't try this at home, though.  ;-)


--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+u...@dartlang.org.



--
Erik Ernst  -  Google Danmark ApS
Skt Petri Passage 5, 2 sal, 1165 København K, Denmark
CVR no. 28866984

Erik Ernst

unread,
Apr 5, 2015, 8:54:50 AM4/5/15
to Paul Brauner, Bob Nystrom, Kenneth Endfinger, core...@dartlang.org
Hi all,

as a followup on the idea that I suggested yesterday, here are a couple of ideas about how the production of deconstruction objects could be declared, and how it would work.

This illustrates that there is no need to have any particular type relation between the representation type and the view type (a Rectangle does not contain any Points), and also that the view objects can provide both read and write access to the representation object(s), based on simple, standard Dart programming and suitable "wrapper" objects.

  abstract class Point {
    int x, y;
    Point(this.x, this.y);
  }

  /// Demonstrate deconstructor declaration.
  class Rectangle {
    int topLeftX, topLeftY, bottomRightX, bottomRightY;
    Rectangle(this.topLeftX, this.topLeftY, this.bottomRightX, this.bottomRightY);

    // "~" declares a deconstructor; argument default value syntax
    // is used to specify values to assign during pattern matching.
    // We can use named or unnamed deconstructors, just like constructors.
    ~ Rectangle.asPoints(
        Point ul = new PointTopLeft(this),
        Point lr = new PointBottomRight(this));
  }

  /// Support a Point view of the top left coordinates of a Rectangle
  class PointTopLeft implements Point {
    Rectangle _rectangle;
    PointTopLeft(this._rectangle);
    int get x => _rectangle.topLeftX;
    set x(int x) => _rectangle.topLeftX = x;
    int get y => _rectangle.topLeftY;
    set y(int y) => _rectangle.topLeftY = y;
  }

  /// Support a Point view of the bottom right coordinates of a Rectangle
  class PointBottomRight implements Point {
    Rectangle _rectangle;
    PointBottomRight(this._rectangle);
    int get x => _rectangle.bottomRightX;
    set x(int x) => _rectangle.bottomRightX = x;
    int get y => _rectangle.bottomRightY;
    set y(int y) => _rectangle.bottomRightY = y;
  }

  Rectangle rectangle = new Rectangle(100, 100, 200, 200);

  var Rectangle.asPoints(Point ul, Point lr) = rectangle;
  lr.y += 50;
  // corresponds to
  Point ul = new PointTopLeft(rectangle);
  Point lr = new PointBottomRight(rectangle);
  lr.y += 50;

Of course, we can add syntactic sugar (say, omitting the types of `ul` and `lr` and taking them from the declaration of asPoints, and implicitly declaring deconstructors corresponding directly to Foo(this.bar, this.baz) style constructors).  My first focus, however, was to make it possible to support a rather broad range of cases, as long as we are willing to write the code that does the job.  The only new elements here are the deconstructor declarations and the usage of deconstructors "whose arguments are declarations" as patterns.

Note that this notion of deconstructors and patterns easily extends to nested patterns. Assume that we have syntactic sugar that implictly declares the following in Point (or assume that we wrote it explicitly):

  ~ Point(int x = x, int y = y);

we could then use nesting:

  var Rectangle.asPoints(Point(a, b), lr);  // Types of a, b taken from ~ decl.
  // corresponds to
  Point freshName = new PointTopLeft(rectangle);
  int x = freshName.x;
  int y = freshName.y;
  Point lr = new PointBottomRight(rectangle);

As usual, we have to manage mutability carefully: `lr` provides read/write access to rectangle.bottomRightX-and-Y, but `x` and `y` are just simple ints whose initial value is obtained from rectangle.topLeftX-and-Y.  It is of course wasteful to create a PointTopLeft only to read its `x` and `y` a single time, but a smart compiler might be able to reduce this to a simple `int x = rectangle.topLeftX`, and avoid creating the PointTopLeft at all.

I suppose there is no real need for supporting patterns as method arguments:  Rather than writing 5 different methods, taking 5 different (lists of) patterns as arguments, we would have a single method taking plain old-fashioned non-pattern arguments with a body along the lines of

  if (var ..Pattern1..) {
    ...
  } else if (var ..Pattern2..) {
    ...
  } ...

I think it is worthwhile to try to make pattern matching fit into an imperative setting (in particular: to enable read/write access when desired), rather than trying to fit Dart into a purely functional strait-jacket.  We just need some robust, well-understood design patterns and good judgment.  ;-)

Brian Schardt

unread,
Jul 13, 2018, 7:09:21 PM7/13/18
to Dart Core Development, po...@google.com, rnys...@google.com, kaend...@gmail.com
Where are we on this. Is this something that is actively and currently being considered to be added to dart?

Bob Nystrom

unread,
Jul 13, 2018, 7:09:35 PM7/13/18
to Brian Schardt, Dart Core Development, Paul Brauner, Kenneth Endfinger
This thread is very old. :)

Since then, we have been mostly focused on moving to the new sound type system and getting Dart 2 done and out the door.

There is still interest on the language team for destructuring, though that term can mean a lot of different things to different people. I'm not sure where it lies in priority compared to other stuff.

Cheers!

– bob

Reply all
Reply to author
Forward
0 new messages