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

Closed
lrhn opened this issue Feb 5, 2019 · 61 comments
Closed

Destructuring #207

lrhn opened this issue Feb 5, 2019 · 61 comments
Labels
feature Proposed language feature that solves one or more problems patterns Issues related to pattern matching.

Comments

@lrhn
Copy link
Member

lrhn commented Feb 5, 2019

Destructuring is now parts of the patterns feature, and tracked by the specification of that (#546).
No further updates are expected to this issue, it's kept open only as a representation of the status of that particular part of pattern matching.


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 Proposed language feature that solves one or more problems label Feb 5, 2019
@andreashaese
Copy link

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)
}

@roman-vanesyan
Copy link

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
Copy link

Yup, that's covered in the proposal.

@thomasio101
Copy link

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.

@thomasio101
Copy link

thomasio101 commented Feb 19, 2019

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

@thomasio101
Copy link

@thomasio101
Copy link

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
Copy link

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
Copy link

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 :)......)

@lrhn
Copy link
Member Author

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.

@lrhn
Copy link
Member Author

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).

@gamebox
Copy link

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).

@lrhn
Copy link
Member Author

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
Copy link

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

public typealias Void = ()

@lrhn
Copy link
Member Author

lrhn commented Mar 13, 2019

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

@lrhn
Copy link
Member Author

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.

@lrhn
Copy link
Member Author

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.

@thomasio101
Copy link

@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?

@roman-vanesyan
Copy link

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

@rrousselGit
Copy link

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;

@rrousselGit
Copy link

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];

@munificent
Copy link
Member

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
Copy link

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

@rrousselGit
Copy link

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
Copy link

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.

@thomasio101
Copy link

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.

@Levi-Lesches
Copy link

Okay, which function is a duplicate of the other: this, or #68? Either way it seems if you fix one, you fix the other.

@tjx666
Copy link

tjx666 commented Apr 22, 2021

Very useful syntax!

@munificent
Copy link
Member

Okay, which function is a duplicate of the other: this, or #68? Either way it seems if you fix one, you fix the other.

Neither are strictly duplicates. There are some languages (Lua, Go) that allow multiple returns without actually reifying them into a single destructurable object.

@mat100payette
Copy link

What's the status on this ? Builtin tuples and destructuring has been a thing for like half a decade in C# and other languages.

@munificent
Copy link
Member

We haven't had time to work on it recently. Destructuring is on my plate, and static metaprogramming has been keeping me busy. I am hoping to resume working on it fairly soon.

@mat100payette
Copy link

We haven't had time to work on it recently. Destructuring is on my plate, and static metaprogramming has been keeping me busy. I am hoping to resume working on it fairly soon.

Lovely. Thanks for the quick update!

@PaulHalliday
Copy link

Excited for this. Thanks for the hard work!

@Nikitae57

This comment was marked as off-topic.

@leafpetersen leafpetersen removed this from Being discussed in Language funnel Mar 16, 2022
@ricardoboss
Copy link

Is this no longer under discussion? What state is this issue in?

@lrhn
Copy link
Member Author

lrhn commented Jun 9, 2022

Destructuring is now part of the more general "patterns" design proposal.
https://github.com/dart-lang/language/issues?q=is%3Aissue+is%3Aopen+label%3Apatterns

As you can see, it's actively being discussed.

@lrhn lrhn added the patterns Issues related to pattern matching. label Jun 9, 2022
@jodinathan
Copy link

Destructuring is now part of the more general "patterns" design proposal. https://github.com/dart-lang/language/issues?q=is%3Aissue+is%3Aopen+label%3Apatterns

As you can see, it's actively being discussed.

I guess it would be good to edit the original post to add this info

@Leedehai
Copy link

Leedehai commented Oct 5, 2022

Hello.. Despite the "patterns" label as mentioned in #207 (comment), it still looks as if the discussion is not moving forward.. given there's no update on the decision.

Is it safe to consider it as being indefinitely shelved?

@lrhn
Copy link
Member Author

lrhn commented Oct 6, 2022

I think this issue can be closed as #546 has taken over.

@lrhn lrhn closed this as completed Oct 6, 2022
@Leedehai
Copy link

Leedehai commented Oct 6, 2022

Hi I don't think this issue should be closed :) otherwise it will appear in #546 as if the item has been completed [1], unlike the other items like [2].

Maybe a better strategy is to leave the issue open, but add a notice on the first post that this is being tracked in #546 and no more updates are expected on this post itself.

[1]
Screen Shot 2022-10-06 at 15 28 50
[2]
Screen Shot 2022-10-06 at 15 29 05

@lrhn
Copy link
Member Author

lrhn commented Oct 7, 2022

Ack, let's do it that way.
I'm not sure it matters either way, and I fear we'll just forget to close this issue when patterns land. So let's not forget.

@lrhn lrhn reopened this Oct 7, 2022
@munificent
Copy link
Member

I'm not sure it matters either way, and I fear we'll just forget to close this issue when patterns land. So let's not forget.

I'll go through everything labeled "patterns" and clean it up once it ships. :)

@m-fire
Copy link

m-fire commented Mar 10, 2023

How's it going? No disassembly, it's a pain :)

@lrhn
Copy link
Member Author

lrhn commented Mar 10, 2023

It's going awesomely, check: https://medium.com/dartlang/dart-3-alpha-f1458fb9d232

@munificent
Copy link
Member

Closing this because records and patterns are now enabled by default on the main branch of the Dart SDK! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems patterns Issues related to pattern matching.
Projects
None yet
Development

No branches or pull requests