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 #207

Open
lrhn opened this issue Feb 5, 2019 · 50 comments
Labels

Comments

@lrhn
Copy link
Member

@lrhn lrhn commented Feb 5, 2019

Dart could have some way to easily "destructure" composite values.

Example proposal from the DEP process: https://github.com/DirectMyFile/dep-destructuring (by @kaendfinger).

@lrhn lrhn added the feature label Feb 5, 2019
@lrhn lrhn mentioned this issue Feb 5, 2019
@andreashaese

This comment has been minimized.

Copy link

@andreashaese andreashaese commented Feb 6, 2019

For more inspiration, I'll add some Swift examples.

Tuples & tuple desctructuring

let tuple = (1, "A")
let (x, y) = tuple

Tuple dot notation

let triple = (1, 2, 3)
let (a, b) = (triple.0, triple.2) // a = 1, b = 3

Binding with pattern matching

switch tuple {
case let (x, y) where x > 0 && y == "A":
    print(x, y)
    
case let z:
    print(z)
}

Binding with for loops

let dictionary = [1: "one", 2: "two", 3: "three"]

for (key, value) in dictionary {
    print(key, value)
}

Wildcards

for (_, value) in dictionary {
    print(value)
}

switch triple {
case let (a, _, _):
    print(a)
}
@vanesyan

This comment has been minimized.

Copy link

@vanesyan vanesyan commented Feb 10, 2019

BTW it will allow a sweet feature like swapping data without necessity of temporal variable, e.g.:

Currently:

int a = 1;
int b = 2;

int tmp = a;
a = b;
b = tmp;

with destructuring:

int a = 1;
int b = 2;

[a, b] = [b, a];

a; // 2
b; // 1
@andreashaese

This comment has been minimized.

Copy link

@andreashaese andreashaese commented Feb 10, 2019

@thomasio101

This comment has been minimized.

Copy link

@thomasio101 thomasio101 commented Feb 16, 2019

The proposal only details destructuring in the context of assignment, such as in variable assignments/declarations, or in loops. But I'm seeing quite a potential here for using destructuring in the context of function/constructor/accessor arguments, as is the case in Python.

Examples

These examples have all been written in Python.

Iterable unpacking

def my_function(x, y):
  print(x)
  print(y)

my_tuple = (1, 2)

my_function(*my_tuple)

This prints the following;

1
2

Mapping unpacking

def my_function(x, y):
  print(x)
  print(y)

my_dictionary = {'x':1, 'y':2}

my_function(**my_dictionary)

This prints the following;

1
2

It also works in reverse;

my_dictionary = {'y':2, 'x':1}

my_function (**my_dictionary)

This will print the following

1
2

NOTE See the Python specifications for a more comprehensive look at the "unpacking" functionality

Issue

The problem that this concept poses, is the proposal's lack of an operator like the * or ** operators from Python. This lack of operators means there's no distinguishment between destructuring a value to parameters and passing the value itself as an argument.

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Feb 18, 2019

.. several posts irrelevant to the issue of destructuring - deleted by the author

1 similar comment
@thomasio101

This comment has been minimized.

Copy link

@thomasio101 thomasio101 commented Feb 19, 2019

.. several posts irrelevant to the issue of destructuring - deleted by the author

@thomasio101

This comment has been minimized.

@thomasio101

This comment has been minimized.

Copy link

@thomasio101 thomasio101 commented Feb 19, 2019

@tatumizer I've already published a test on the repo which eliminates the possibility of a significant performance differential between positional, named and mixed arguments in the verbose method.

I will also be doing other performance tests in the repository, but maybe it would be interesting if you open an issue on there with your test's source code? That way I can also provide some feedback and possibly replicate the test my self to compare the results.

EDIT

  • In case you read over the link to source code in my report. Here it is.
  • I've also made some code to generate Markdown tables, you can find it here - I guess it might come in useful for visualizing your own results.
@thomasio101

This comment has been minimized.

Copy link

@thomasio101 thomasio101 commented Feb 19, 2019

I was aware of that, my report's hypothesis states that I did not expect any significant difference between named and positional arguments in verbose code.

The reason I tested it was just to make sure we weren't going off a wild guess.

@thomasio101

This comment has been minimized.

Copy link

@thomasio101 thomasio101 commented Feb 19, 2019

That sounds like a good idea, anyways, I'm going to make a generic API for running tests, although I need to see if the overhead is not to much (maybe I could over-engineer this a bit with a web panel with fancy graphs and the ability to monitor tests remotely :)......)

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Feb 19, 2019

I opened an issue in your repo to move further discussion from this thread. Let's communicate via this issue, I will be happy to help you to build a framework
Let's delete our messages here, OK? (Only you can delete your posts)

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Feb 19, 2019

@lrhn: Property destructuring, as defined by this proposal, is ambiguous. List itself is an object having a number of properties, so in the expression var [first, length] = [ 0, 1 ]; we have 2 candidates for "length" assignment: [0, 1][1] (which is 1) or [0, 1].length (which is 2)

@tatumizer tatumizer mentioned this issue Feb 20, 2019
@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 7, 2019

Dart has a syntax problem WRT destructuring. If we allow this:
var [String a, int b] = ["Hello World", 5];
then, for consistency, var String a = "blah" has to be allowed, too.
Further, what about inferred types?
var [a, b] = ["Hello World", 5];
Is it a valid declaration? Probably it is, but then
var [String a, b] = ["Hello World", 5];
Is "b" here also a String, or dynamic, as suggested by "var"? Or inferred as "int"?
And what about this:
int [a, b] = [0, 2];
Is it a valid declaration?

@lrhn

This comment has been minimized.

Copy link
Member Author

@lrhn lrhn commented Mar 8, 2019

Lists are not tuples, so I would't use var [a, b] = ["Hello world", 5]; for tuples. The expression on the right-hand side is an existing Dart expression with type List<Object>.
Doing list deconstruction this way is possible (and risks throwing if the list expression is not a literal and ends up not having two elements).

For a tuple-based syntax like var (a, b) = ("Hello world", 5); I'd expect the RHS to have type String * int and infer String for a and int for b.

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 8, 2019

Probably, destructuring for lists doesn't make much practical sense anyway.
Suppose we have tuples

print(("hello", 5).runtimeType); // what does it print? "String*int"?
String*int (a,b); // is it a valid declaration? 
var (a,b); // is it a valid declaration? is it "dynamic*dynamic" ?

I think all answers are "yes", juts wanted to double-check

@lrhn

This comment has been minimized.

Copy link
Member Author

@lrhn lrhn commented Mar 8, 2019

I won't put my money on any specific tuple type syntax yet, too many options are open. I intended String * int as the semantic type here.

The point of tuples, maybe even named tuples like (1, "yes", reserve: "table") is that a tuple has a static type, so we can spread it in a parameter list and know statically which slots it will apply to, and whether it matches the required parameter count and types.

You can spread a list in a list, or a map in a map. You can't just spread a map or list in a parameter list because it's a different structure with different requirements (like a per-element type), but a tuple can be an abstraction with the same structure as a parameter list, so it would make sense to spread a tuple in another tuple, or in a parameter list.

(Maybe (String a, int b) p = someTupleOperation(); print(p == (a, b)); // true).

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 8, 2019

Tuples are good for functional programming. I played with some variants the other day, came up with notation #(a, b) to make it shout louder, b/c raw parentheses hurt readability IMO. Not sure.
Anyway, tuples make it possible to turn Iterable.fold into universal looping mechanism, e.g.

Iterable<int> codePoints(String string) {
  processNextChar(#(List accum, int prev), int char) { // parameter - tuple, with automatic destructuring
    if (prev != null && isLeadSurrogate(prev) && char != null && isTailSurrogate(char)) {
      return #(accum..add(combineSurrogates(prev, char)), null); // returning tuple, denoted as #(value, value)
    } else {
      return #(prev == null ? accum : accum..add(prev), char); // returning tuple
    }     
  }

  return string.codeUnits.followedBy(<int>[null]).fold(#([], null), processNextChar)[0]; // passing initial tuple in "fold"
}

Still, I like the version with sync* more - the example just illustrates the point about fold. It's probably a standard pattern in functional programming, I assume.

@gamebox

This comment has been minimized.

Copy link

@gamebox gamebox commented Mar 12, 2019

Tuples with () syntactical representation appear in a number of languages where parens are also used for declaring parameter lists. See Scala, Rust, and Swift to name a few. I used them in Scala for years and found no readability problem with them(data point of 1, I know). Now, that form can present some interesting challenges in function declaration syntax of the sort Dart has(C style), but it should definitely be manageable. I will also say being able to spread a tuple into a parameter list is intriguing as an idea, but I think it could be confusing to users. There is just a lot of edge cases where the behavior would be hard to intuit. I also don't know if I've ever encountered a languages with a partially named Tuple before, usually Tuples and Structs/Records are very distinct(even if the literal is similar syntactically).

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 12, 2019

@gamebox : without "#" as a prefix for tuple, as in #(1, "Hello"), how do you write 1-tuple? 0-tuple?

@lrhn

This comment has been minimized.

Copy link
Member Author

@lrhn lrhn commented Mar 13, 2019

You write a one-tuple as (1). Through no coincidence, a one-tuple is a value.
A zero-tuple is harder, but () is an option. The next question is whether you ever need a zero-tuple, it's a singleton type with no useful behavior, so you might as well use null.
(This assumes that there is no type relation between tuples with different structures, so a zero-tuple type is never a sub- or super-type of a one tuple type).

@andreashaese

This comment has been minimized.

Copy link

@andreashaese andreashaese commented Mar 13, 2019

In Swift, you're using the empty tuple basically everywhere without even noticing:

public typealias Void = ()
@lrhn

This comment has been minimized.

Copy link
Member Author

@lrhn lrhn commented Mar 13, 2019

If we had tuples, maybe we should just make null be an alias for the zero-tuple.

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 13, 2019

The question whether expression () can be considered as a synonym of null is an interesting one.
In some languages, nil is just another name for (), but in Swift (I just checked it out), it doesn't seem to be the case (not sure).

In dart, we have an intriguing possibility to treat 0-tuple () as Nothing - which I believe, is a necessary concept. If we agree to model tuples by analogy with parameter lists of functions (and the idea of including named elements into the definition certainly points in this direction), then, quite naturally, function with no parameters corresponds to 0-tuple. On the other hand, when asked what we pass as a parameter to a function requiring no parameters, the only logical response we could give would be "Nothing". But the story doesn't end here.

Dart has some mysterious concept called "bottom type", which is not expressed in the language, but shows up in a language spec. Can it be that it's exactly *that" type? Which leads us to a bold
Conjecture: Nothing = () = bottom type

If we could agree on that, then we get an answer to a long-standing question about the type of ?null in null-elimination operator (see #219)

@lrhn

This comment has been minimized.

Copy link
Member Author

@lrhn lrhn commented Mar 13, 2019

It's unlikely that the zero-tuple type () will be bottom because the zero-tuple type contains one value (the empty tuple) and the bottom type is necessarily empty.

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 13, 2019

It can't be! If we have foo() {...}, and we call it foo(), what is inside parentheses? I can't see anything there, so it must be Nothing, or completely non-existent thing, which is as good (or as bad) as "bottom".
"Nothing" is a tricky subject though, because the very act of mentioning it makes it a kind of "value" - probably, in the second-order logic. And it's exactly in that sense () is a value, but so is "bottom". No?

@lrhn

This comment has been minimized.

Copy link
Member Author

@lrhn lrhn commented Mar 13, 2019

No.

Ob-digression:

Tuples are product types. If you see types as sets of values (which is a slightly naive mode, but useful) then the tuple type (bool, bool) is the product of the set of booleans with itself (which is a set of pairs: {(x, y)|x in bool & y in bool}). The cardinality of bool * bool, say |bool * bool|, is the product of the cardinalities of the individual sets, |bool| * |bool| (aka. 2 * 2 = 4).

In general, then n tuple of a type, booln, would have cardinality |bool|n.
The one-tuple is the set itself, no surprise there.

From that it follows that the zero tuple, bool0, must have cardinality 1.

Up to isomorphism, all one-element sets are the same, and a one element set is is the neutral element wrt. set products (let unit be the singleton set {?}, then unit * bool is {(?, true), {?, false)} which is isomorphic with bool - inside this set the always-present "?" makes no difference, it's still a two-element set, and all two-element sets are isomorphic too).

The empty set is the neutral element wrt. set union, but an eliminator wrt. set product. It's similar to how 0 and 1 work with multiplication in the non-negative integers.

In this sense, the unit type and the empty type are very different, and bottom is the empty type, while the zero-tuple is the unit type.

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 13, 2019

From that it follows that the zero tuple, bool^0, must have cardinality 1.

OMG!!! Indeed...

But wait... |bool|^0 is 1 because |bool|=2, so we have 2^0 where the inexorable laws of mathematics dictate that the value must be equal to one - I concede that.

However! What if we consider () to be a tuple formed by 0 elements of bottom type? Now, because the cardinality of bottom type is zero, we have 0^0, which evaluates to 0 in one way, and to 1 in another way, so it's not uniquely defined, and now we are free to define it either way, unless someone can demonstrate that our definition leads to a contradiction. My argument is that it's more convenient for us to to select cardinality 0, thus identifying () with bottom type.

The reason such definition would be more convenient is that the alternative (identifying () with null) destroys our elegant theory in which we consider tuples as a general concept underlying parameter lists of functions, and as soon as the list is empty, the call foo() is certainly not equivalent to foo(null) in dart. We can easily imagine a different language built around the notion of tuples from ground up, where foo() and foo(nil) are indeed equivalent, but this is not our case.

Still no?

@thomasio101

This comment has been minimized.

Copy link

@thomasio101 thomasio101 commented Mar 14, 2019

@tatumizer; aren't empty parameter lists such as in foo() just tuples with zero elements, if we were to fetch the argument list à la JavaScript we'd get an empty collection, not nothing! Or am I getting something wrong here?

@vanesyan

This comment has been minimized.

Copy link

@vanesyan vanesyan commented Mar 14, 2019

we have 0^0, which evaluates to 0

It is not true from mathematical perspective 0^0 commonly evaluated to 1 or less often to undefined, see: https://en.wikipedia.org/wiki/Zero_to_the_power_of_zero

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 14, 2019

Oh, I see where my mistake was: while calling the function, we don't pass the tuple of its parameters as is, but use spread operator "...". So if we have 0-tuple, we would say foo(...()), which is OK. We don't need to identify "()" with bottom type for that. But it won't be convenient to identify () with null either, because we already adopted the notation "...?" while "spreading" a null. The bottom line: maybe in dart, it makes sense to consider "()" to be neither null, nor bottom, but an independent concept - empty tuple? Nothing special about it: we have empty lists, empty maps - will now add empty tuples, one empty thing more, one empty things less - who cares. In fact, I'm never surprised while encountering yet another empty thing, it's much more surprising to find it non-empty (which is indeed rare these days).

@vanesyan : 0^0 is a fuzzy concept indeed, any definitive value you assign to it will lead to contradictions in math (that is, you can show that assuming 0^0=k, you can prove that 0 = 1). But this is true in the specific "language" of numbers! Whether you can arrive at a contradiction or not - depends very much on what sentences you are allowed to form in the language (see Russel's paradox) In the limited linguistic context of dart tuples, I don't think we would have this contradiction.

@rrousselGit

This comment has been minimized.

Copy link

@rrousselGit rrousselGit commented Mar 23, 2019

It'd be interesting to have it for classes too.

Consider the following class:

class Foo {
  String foo;
  int bar;
}

we'd be able to do:

final {foo, bar} = Foo();

which would be equivalent of:

final obj = Foo();
final foo = obj.foo;
final bar = obj.bar;
@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 23, 2019

It'd be interesting to have it for classes too.

There's a problem with "too" part. You can have either object destructuring, or list destructuring, but not both. Otherwise the result of final (length, first) = [10, 1] might be surprising :)

@rrousselGit

This comment has been minimized.

Copy link

@rrousselGit rrousselGit commented Mar 23, 2019

Javascript is a good example of how we can have both.

  • List destructuring:
const [first, second] = [0, 1];
  • Object destructing:
const {length} = [0, 1];
@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 23, 2019

Notation final {a, b} = obj in dart (as opposed to javascript!) could be confusing - it would give you an impression that you are destructuring a map {"a": 0, "b": 1}, which is not the case. The assignment
final (keys, values) = {"a": 0, "b": 1} will work though :-)

@munificent

This comment has been minimized.

Copy link
Member

@munificent munificent commented Mar 27, 2019

Javascript is a good example of how we can have both.

Javascript is sometimes but not always a good example that we can follow in Dart. One key difference is that JavaScript does not distinguish maps (data structures) from objects (instances) while Dart does. That means that, unlike Javascript, we can't naturally use { and } to refer to either maps or objects interchangeably. Consider, for example:

var map = {"length": "not 1!"};
var {length} = map;
print(length); // 1, or "not 1!"?
@rrousselGit

This comment has been minimized.

Copy link

@rrousselGit rrousselGit commented Mar 27, 2019

What about:

var [length] = map; // "not 1!"

vs

var {length} = map; // 1

?

This follows the logic of
map["length"] vs map.length

And it would be the same for any operator [] override where the accessor is either a string or an integer

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 27, 2019

Special syntax is required to handle 3 cases: 1) list destructuring 2) map destructuring 3) object destructuring (extracting the properties from an object, as opposed to the items from a collection)
The first 2 cases can be handled by javascript-like syntax. For the third one, we need something else, e.g.

var *{length, first} = [1, 2, 3];   //  asterisk before {
var *{length} = {"a": 0, "b": 1};

Not sure I like it. Maybe we can live without object destructuring?

@rrousselGit

This comment has been minimized.

Copy link

@rrousselGit rrousselGit commented Mar 27, 2019

Something worthy of mention is that it is possible to put anything as keys of a Map.

IMO the tree cases are not List/Map/Objects. But instead:

  • foo[0]
  • foo["string"]
  • foo.property

I would expect the following to work:

var [foo, bar] = {0: 'foo', 1: 'bar'};
print('$foo $bar'); // foo bar

Which means the behavior of destructuring is not based on the type of the object, but its operator [] override.


This has a few implications.

First, it means that any objects that override operator [] can plug into the destructuring API. Which means we can have:

class Foo {
  int operator [](int index) {
    return index;
  }
}

var foo = Foo();

var [first, second] = foo;
print('$first $second'); // 1 2

Secondly, it means that var [bar] = foo / var {bar} = foo are completely unrelated to List/Map literal.

Their meaning could be different:

  • var [bar] = foo is for anything related to operator []
  • var {bar} = foo is for accessing properties

Since Dart is typed, the behavior of [] destructuring could depend on the type of the right operand.

Such that we have:

  • T operator [](int accessor)
var [first, second] = {0: 'foo', 1: 'bar'};
print('$first $second'); // foo bar
  • T operator [](String accessor)
var [foo, bar] = {"foo": 42, "bar": 24};
print('$foo $bar'); // 42 24
  • member access
var {length} = {};
print(length); // 0

The case of Tupples is unchanged because they can't be accessed with operator [].

@gamebox

This comment has been minimized.

Copy link

@gamebox gamebox commented Mar 27, 2019

I don't see the need for this special syntax. The destructuring syntax should mirror the literal syntax, with non literals being bound variables. For instance, going back to Scala, there is no List literal. Instead there is a List unapply function, which is kind of like a reverse apply function(or constructor). So, to destruct a list of two elements, it looks like this:

val List(a, b) = someList

Similarly with Map (where there is also no literal syntax:

val Map("a" -> 0, "b" -> 1) = someMap

It's nice in the fact that it is obvious what you are trying to destructure.

In other languages with nice facilities for destructuring and pattern matching, data structures for which there are literals(which is usually about everything) things map very cleanly. For example, Elixir - a really popular language at the moment:

some_list = [1, 2, 3]
[a, b, c] = some_list
some_map = %{"a" => 0, "b" => 1}
%{"a" => a, "b" => b} = some_map
some_keyword_list = [a: 0, b: 1] # This is special Elixir data structure, the keys are atoms
[a: c, b: d] = some_keyword_list
a_tuple = {:ok, [1, 2, 3]} # :ok is an atom
# The next two are both ok
{:ok, list_from_tuple} = a_tuple
{:ok, [a_from_list, b_from_list, c_from_list]} = a_tuple

So if the desire is to have similar facilities here (which I'm not assuming), it could and maybe should like like the following in Dart:

final someList = [1, 2, 3];
final someMap = {"a": 0, "b": 1};
final someClassInstance = SomeClass(param1: "a", param2: 3);

final [one, two, three] = someList;
final [head, ...tail] = someList;
final {"a": zero, "b": mapOne} = someMap; // How to represent non string keys?  In JS, an object is an associative array of string -> JSValue
final SomeClass(param1: instanceA, param2: instanceThree) = someClassInstance;

Now there are still problems here. Elixir is a dynamic language, with a philosophy of "let it crash" so these sorts of destructurings that may in fact raise can do so and the system will chug along. In Scala, this type of naked destructuring is discouraged, except for class instances, since the object does in fact exist and there are certain guarantees around the values exposed through unapply existing as well.

That's kind of a long winded way of saying that most of these naked destructuring patterns are not well suited for a static, strongly typed language. What is useful on the other hand is to take the thoughts and conversation around destructuring and apply it a sane system of pattern matching. That's my opinion, but I think it would be a footgun to allow native destructuring of the type Javascript and Elixir(dynamic languages) give in a language like Dart.

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 28, 2019

The problem is not to come up with the syntax, but to come up with the syntax that fits in dart's style - e.g. by reusing some patterns already established elsewhere in the language.
I feel that the javascript-like syntax like

var [ x, y ] = [ 1, 2];
var { a, b } = {"a": 0, "b": 1};

fits in dart nicely - because it follows the "template" of using [...] and {...} elsewhere. E,g similar notation is used to distinguish between positional parameters from named parameters in method declarations.

The problem is only with object destructuring. We have to find the syntax that "rhymes" with something else in dart (as opposed to scala or elixir). That's why I don't like var *{a, b} = obj - asterisk looks arbitrary here b/c we don't have a precedent where it occurs in a similar sense elsewhere in dart.

Good news: there's hope! How about this:

var ..{x, y} = obj;

With double dot, we have a nice mirror for cascade. E.g.

var ..{x, y} = Point()..x=1..y=2;

WDYT?

@thomasio101

This comment has been minimized.

Copy link

@thomasio101 thomasio101 commented Mar 28, 2019

I don't think it's too bad @tatumizer, but are you sure that this does not conflict with the current assignment syntax? Or is this mitigated by the use of curly brackets ({})?

I'm sure we're onto something here, but I think we need to make sure that we've thought of the edge cases so that the syntax we come up with is adequately robust.

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 28, 2019

The use of braces or brackets followed by assignment '=' makes it unambiguous IMO.
We don't use destructuring without assignment - it wouldn't make any sense.
I couldn't come up with any counterexamples yet.

@gamebox

This comment has been minimized.

Copy link

@gamebox gamebox commented Mar 28, 2019

@tatumizer I think that your first example of your proposed syntax expose it's flaw. A reader of the program needs to look up the declaration of obj to understand what is being destructured.

Destructuring is by definition about breaking down the structure of a value and assigning elements of that structure to some local binding(variable). In a language like Dart, that has to include the Class. Using a constructor like syntax in consonant with that reality, and to use your own terms, "rhymes" with a core Dart syntactical construct. The same thing with the other patterns I mentioned - they directly mirror the form of the literal syntax for the type of the value.

I think the .. does not make intuitive sense even if this sort of form were to be adopted. It is clear that now, in Dart syntax .. will be used in RHS expressions for "spreading" the values of a collection of a certain type into another collection of a covariant or equal type. What would be the intuitive LHS function of this syntax? I think it could be obviously to "collect" any other elements of a value into a single binding, i.e., the following declaration for lists:

final [_, _, ...rest] = someList; // Assuming someList = [1, 2, 3, 4, 5, 6, 7 , 8]
print(rest); // prints "[3, 4, 5, 6, 7, 8]"

as well as the following for maps:

final {"a": a, ...restOfMap} = someMap // Assume someMap is a Map<String, T> with a MapEntry("a", 0)
print(a); // prints "0";
print(restOfMap); // prints a map with the other MapEntries of someMap

and sets(which would be ambiguous with your syntax):

final {x, y} = someSet; // Assuming someSet = <String>{"a", "b"}
print(x); // prints "a";
print(y); // prints "b";

For object types that do not have a literal syntax, you would use a constructor syntax, with the arguments available for matching being defined by some cognate of an unapply method. I like the term destructor, but recognize that one day, Dart might support destructors in the C++ sense, so will stay away form it and refer to unapply for now. Let's assume a mutable class that is rather simple for the time being:

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

  unapply Point({this.x, this.y});

  double x;
  double y;
}

With this, a value of type Point could be deconstructed easily using the unapply constructor, which is only available in the LHS of a variable binding(and in a hypothetical future pattern matching context as well):

var p = Point(x: 1.0, y: 2.0);
...
p.x = 3.0;

final Point(x: x, y: y) = p;
print(x); // prints "3.0";
print(y); // prints "2.0";

You'll notice though, as I said before that all of these present opportunities for problems with null (in the current Dart context), or exceptions (in a NNBD Dart world) if the values do not conform to the proposed structure. The obvious problems like trying to destructure a map with the list syntax could obviously be caught by analysis, but the specific form could not necessarily be statically analyzable. Hence my caution that these sorts of destructurings should only happen in a "pattern matching" context where one is forced to handle that possibility in an exhaustive fashion. If not a full-blown "match" expression, then maybe some sort of syntactic sugar for a construct like:

try {
  final [x, y, ...rest] = someList
  ... // some things to do if the match succeeds
} catch (e) {
  .. // some things to do if the match fails
}

Maybe something on the lines of:

with(final [x, y, ..rest] = someList) {
  // match succeeds
} else {
  // match fails
}

of course a preference being something along the lines of:

match(someList) {
  case [a, b, ..rest]:
    //match succeeds
  default:
    //match fails
}

With match being an expression.

@thomasio101

This comment has been minimized.

Copy link

@thomasio101 thomasio101 commented Mar 28, 2019

@gamebox, my preference goes to the "match" expression. Would this also allow for multiple cases, so a programmer can handle different structures the program will encounter?

@gamebox

This comment has been minimized.

Copy link

@gamebox gamebox commented Mar 28, 2019

@thomasio101 Of course, say we are matching on a list:

match(someList) {
  case [a, b, ..rest]:
    // do something, eq. to if(someList.length > 2) { final a = someList.first; final b = someList[1]; final rest = someList.sublist(2) ... }
  case [a, ..rest]:
    // do something, eq. to if(someList.length > 1) { final a = someList.first; final rest = someList.sublist(1); ... }
  case [a]:
    // do something, equivalent to if (someList.length == 1) { final a = someList.first; .... }
  case []:
    // do something, equivalent to if (someList.isEmpty)
  // default case not needed since all forms of a list have been covered
}

You can see that the logic for this sort of construct would be much more difficult to reason about without this functionality. But destructuring is, in my opinion, needed before you can move forward with pattern matching. They are parts of the same concept.

@tatumizer

This comment has been minimized.

Copy link

@tatumizer tatumizer commented Mar 29, 2019

You can see that the logic for this sort of construct would be much more difficult to reason about without this functionality.

Are you sure the code below is much more difficult? To me, it looks like the same thing :)

main() {
  switch (list.length) {
  case 0:
     // do something
  case 1:
    var [a] = list; // do something
  case 2:
    var [a, b] = list; // do something
  default: 
    var [a, b] = list, rest = list.sublist(2); // do something
  }
}  

We need to add "break" statements here, and this makes the program ugly. But this is fixable (e.g. by introducing more modern variant of switch, with "->" instead of ":")

@gamebox

This comment has been minimized.

Copy link

@gamebox gamebox commented Mar 29, 2019

That example doesn't necessarily make things incredibly better in that obviously contrived example, but consider this example:

final pointList = <Point>[
  Point(x: 1, y: 2),
  Point(x: 4, y: 3),
  .... // Some more Point innstances here
];

match(pointList) {
  case [Point(x: x1, y: y1), Point(x: x2, y: y2), ..rest]:
    Point(x: x1 + x2, y: y1+y2);
  case [point]:
    point;
  default:
    null;
}

Here the intent is clear due to the concision available with this sort of syntax, even if it is unclear in a floating context like this exactly why we are adding the first two points of a list of points.

@jkmpariab

This comment has been minimized.

Copy link

@jkmpariab jkmpariab commented Aug 13, 2019

another use case for this functionality is passing constructor arguments to routes when using onGenerateRoute without defining a redundant class to wrap arguments

@edwjusti

This comment has been minimized.

Copy link

@edwjusti edwjusti commented Oct 26, 2019

One common use for me coming from javascript is awaiting multiple futures and then destructuring the results by assigning them to variables like so:

const [user, purchases] = await Promise.all([
  fetchUser(),
  fetchPurchases()
]);

It would be handy to have something like that on dart for that specific use so that I don't block my async function by running my futures in parallel and easily extracting the result values to variables.

@raveesh-me

This comment has been minimized.

Copy link

@raveesh-me raveesh-me commented Nov 14, 2019

Faced the need for this right now!


  @override
  Widget build(BuildContext context) {
    return PageView(
      controller: _controller,
      dragStartBehavior: DragStartBehavior.down,
      children: <Widget>[
        Center(),
        ...currentMap.entries.map((mapEntry) => Viewpage()).toList();
      ],
    );
  }

would love to destructure the MapEntry in the parameter itself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.