Skip to content

Easier loop for maps #4298

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

Open
HosseinYousefi opened this issue Mar 19, 2025 · 26 comments
Open

Easier loop for maps #4298

HosseinYousefi opened this issue Mar 19, 2025 · 26 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@HosseinYousefi
Copy link
Member

Currently we can loop over maps via map.entries:

for (final MapEntry(:key, :value) in map.entries) {
  // ...
}

It would be nice to have either a syntax sugar or have Map<K, V> implement Iterable<(K, V)> so that we can do

for (final (key, value) in map) {
  // ...
}
@HosseinYousefi HosseinYousefi added the feature Proposed language feature that solves one or more problems label Mar 19, 2025
@dcharkes
Copy link
Contributor

cc @lrhn

@lrhn
Copy link
Member

lrhn commented Mar 19, 2025

Not a new request.

I have some ideological oppositions to it (a map is not just a sequence of pairs!)

It's definitely not an Iterable<(K, V)>, the type of forEach is wrong. It can at most have an Iterable, like get entryPairs.

I'd then argue that if we allow that then it should be ({K key, V value}).
I'm also perfectly aware that that won't be what people want, so we might as well not do it then.

In practice, I see why you want to go through entries while naming the keys and values by what they mean in your domain, it's not like key and value are adding much information, and an Iterable of positional pairs is the least intrusive easy to do that.

So, maybe.

@FMorschel
Copy link

Just in case you aren't aware @HosseinYousefi , we have another proposal for using _ instead of MapEntry too. Not the best option but may help a bit in the meantime if it gets in the language first.

@HosseinYousefi
Copy link
Member Author

Not a new request.

I figured but couldn't find an issue about it.

I have some ideological oppositions to it (a map is not just a sequence of pairs!)

Iterating over a map is iterating over a sequence of pairs though.

In practice, I see why you want to go through entries while naming the keys and values by what they mean in your domain, it's not like key and value are adding much information, and an Iterable of positional pairs is the least intrusive easy to do that.

You hit the nail on the head here. Writing key and value doesn't really add much clarity. The concept of maps and dictionaries are universal enough and the order of key-first-value-second is familiar to programmers coming from other languages as well.

+1 to having a get entryPairs, it's analogous to Python's items().

@lrhn
Copy link
Member

lrhn commented Mar 19, 2025

Since this is in the language repository, I'll take it as a request for a for loop that can loop over maps, rather than a library feature that creates an iterable of pairs.

What would that look like:

for (var k, v in map) { ... }

The map is-not-an Iterable, so there is a type hint.

That's not enough if someone wants to iterate a dynamic, so we also make the variable-or-declaration have two variables.
That is: Either the part before in is two variables separated by a comma, or it's a variable declaration declaring two variables. (If you want them to have different types, ... you're stuck, unless we allow var <declarationPattern>, <declarationPattern>. So let's do that. Then you can do any of:

var map = <int, String>{...};
for (var k, v in map) ...
for (var (int k), (String v) in map) ...
int k; String v;
for (k, v in map) ...

Or we could make it two "variable or declaration"s separated by comma .. then you can do for (int i, String j in map) ... or for (x, int j in map)..., but it's probably too confusing if for (var k, v in map) ... means v should be an existing variable.
So one declaration for two variables it is.

No records introduced at all, no need to allocate them. The semantics would be to iterate .entries and use .key and .value as the initializers for the declared variables.

(I'd be perfectly happy not allowing for/in to use an existing variable, here or in general, and require a declaration before the in. Almost nobody uses a variable there, you can always do for (var _i in ...) { i = _i; ...} to get the same effect, and it makes parsing much simpler.)

@ghost
Copy link

ghost commented Mar 19, 2025

for (var k, v in map)

The types of k, v can be inferred from the statically known type parameters of map. If they are unknown, then it's dynamic, dynamic. Right?

@mmcdon20
Copy link

That's not enough if someone wants to iterate a dynamic, so we also make the variable-or-declaration have two variables. That is: Either the part before in is two variables separated by a comma, or it's a variable declaration declaring two variables. (If you want them to have different types, ... you're stuck, unless we allow var <declarationPattern>, <declarationPattern>. So let's do that. Then you can do any of:

var map = <int, String>{...};
for (var k, v in map) ...
for (var (int k), (String v) in map) ...
int k; String v;
for (k, v in map) ...
Or we could make it two "variable or declaration"s separated by comma .. then you can do for (int i, String j in map) ... or for (x, int j in map)..., but it's probably too confusing if for (var k, v in map) ... means v should be an existing variable. So one declaration for two variables it is.

Honestly I think two separate declarations would be better, meaning you would have to write:

for (var k, var v in map) ...

It's pretty common for maps to have different types for key and value, so it should be easy to express this case

for (int k, String v in map) ...
// vs
for (var (int k), (String v) in map) ...

Also imagine you have the following map.

final Map<String, ({double latitude, double longitude})> map = {
  'Chicago': (latitude: 41.878113, longitude: -87.629799),
  'London': (latitude: 51.507351, longitude: -0.127758),
  'Aarhus': (latitude: 56.162937, longitude: 10.203921),
};

With two separate declarations you could do the following

for (String city, final (:double latitude, :double longitude) in map) ...

With your proposed one declaration for both variables approach, I'm not sure how pattern matching would be supported.

@ghost
Copy link

ghost commented Mar 19, 2025

final Map<String, ({double latitude, double longitude})> map = {
  'Chicago': (latitude: 41.878113, longitude: -87.629799),
  'London': (latitude: 51.507351, longitude: -0.127758),
  'Aarhus': (latitude: 56.162937, longitude: 10.203921),
};

For this map, you just write:

for (var city, coord in map) {
  print(city); // city has type "String"
  print(coord.latitude); // coord has type ({double latitude, double longitude})
  print(coord.longtitude);
}

Both types are inferred from the static type of your map.

@mmcdon20
Copy link

mmcdon20 commented Mar 19, 2025

final Map<String, ({double latitude, double longitude})> map = {
'Chicago': (latitude: 41.878113, longitude: -87.629799),
'London': (latitude: 51.507351, longitude: -0.127758),
'Aarhus': (latitude: 56.162937, longitude: 10.203921),
};
For this map, you just write:

for (var city, coord in map) {
print(city); // city has type "String"
print(coord.latitude); // coord has type ({double latitude, double longitude})
print(coord.longtitude);
}
Both types are inferred from the static type of your map.

Of course that would work, but what if you want the option to do a pattern match instead? Having two separate declarations easily allows for both options.

// with two declarations both are fine
for (var city, var coord in map) ...
for (var city, var (:latitude, :longitude) in map) ... 

// with one declaration
for (var city, coord in map) ... // fine
// how to pattern match on coord? is it supported? what is the syntax?

@ghost
Copy link

ghost commented Mar 19, 2025

  var t=[(k: 1, v:'abc'), (k:2, v:'x')];
  for (var (:k, :v) in t) {
    print('$k $v');
  }

Here, in the case of an iterable, it's supported. This means it has to be supported in a map syntax, too.

@TekExplorer
Copy link

TekExplorer commented Mar 20, 2025

for the record, that would be destructuring, not pattern matching

also, I would say that the least intrusive way of implementing this is to just introduce an extension like

extension MapKVIterable<K, V> on Map<K, V> {
  Iterable<(K key, V value)> get kv => entries.map((e) => (e.key, e.value));
}

and maybe include that in the SDK - naming being the only thing to consider.

for now, anyone here can just go ahead and copy-paste that to their projects.

@mmcdon20
Copy link

Here, in the case of an iterable, it's supported. This means it has to be supported in a map syntax, too.

I would hope that a map-loop syntax would support it. Note that destructuring patterns currently are not supported when you have multiple variables in a single declaration.

// with two declarations
// works
var city = 'Chicago';
var (:latitude, :longitude) = (latitude: 41.878113, longitude: -87.629799);

// with one declaration
// error
var city = 'Chicago', (:latitude, :longitude) = (latitude: 41.878113, longitude: -87.629799);

So you would either need to allow destructuring patterns in multivariable declarations or have two separate declarations.

@ghost
Copy link

ghost commented Mar 21, 2025

var city = 'Chicago', (:latitude, :longitude) = (latitude: 41.878113, longitude: -87.629799);

Any idea of why this is not supported? I thought it can cause some parsing ambiguity, or require a complicated lookahead, but I failed to discover such scenarios.

@lrhn
Copy link
Member

lrhn commented Mar 21, 2025

IIRC, the grammar isn't the problem, more the human experience.

var x = 42, (y, z) = p;

The (y, z) can get far away from the leading var, and without that var it looks exactly like a destructuring assignment.

@ghost
Copy link

ghost commented Mar 21, 2025

Oh, it took me a while to figure out that var (a, b) = (1, 2); is a simultaneous assignment.
Still don't know how to interpret (y, z) = p; (is it a simultaneous assignment, too? what is p ?).
This var (a, b) = (1, 2) could be a trick interview question: What dart feature is used in this declaration?
I would certainly fail (simultaneous assignment is an ancient feature, I long forgot about it)
But the things still don't quite add up.
Because I know that swapping became available only after the introduction of destructuring.
By googling around, I could verify this. Quoting from stackoverflow Q/A:

It [swap] is possible on Dart 3, using record pattern feature.
var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap!
print('$a $b'); // Prints "right left".

So, the first line is a "simultaneous assignment" 1, but the second is destructuring. Good to know! :-)

Footnotes

  1. to verify this is a simultaneous assignment, try var (z)=(1); It works. If it were a destructuring, then (1) without the comma wouldn't have been a record, so the declaration would be flagged. (not sure. The matter is rather subtle :-))

@lrhn
Copy link
Member

lrhn commented Mar 22, 2025

It's not a simultaneous assignment.

var (x, y) = (1, 2);

is a destructuring declaration.
It creates a pair object (1, 2), then it destructures that pair against a record pattern, and assigns the record field values to the pattern variables.

A good optimizing compiler can avoid creating the object.

Without the leading var it would be a destructuring assignment, and the variables had to exist already.

var (x, y) = p;

is exactly the same, only the expression is p, which must still evaluate to a two-element record for the destructuring declaration to be valid.

Swap works, but taken literally it creates a new object before it destructures it.
You can swap as (a, b) = (b, a), (v1: b, v2: a) = (v2: b, v1: a) or even, if they have the same type, [a, b]=[b, a] or {"1": a, "2": b]}={"1": b, "2": a}. (I think, it's so silly I haven't actually tried)

@ghost
Copy link

ghost commented Mar 22, 2025

Then what is var (z)=(1); ? For destructuring, (1) has to be a Record, but the 1-record has a different syntax: (1,)
My whole theory was based on this observation!

@mmcdon20

This comment has been minimized.

@ghost
Copy link

ghost commented Mar 22, 2025

It's so confusing.

var x=1; // x=1 - naive form
var x=(1); // x=1, might result from macro substitution
var (x)=(1); // x=1
var (x)=1; // x=1
var (x,)=(1,); // x=1
var (x)=(1,); // a totally different thing! x= (1,)

Some of these forms should be flagged IMO.

@mmcdon20

This comment has been minimized.

@TekExplorer
Copy link

It's so confusing.

var (x)=(1); // x=1
var (x)=1; // x=1
var (x,)=(1,); // x=1
var (x)=(1,); // a totally different thing! x= (1,)

Some of these forms should be flagged IMO.

They do. You should get the unnecessary parenthesis lint triggered. Not sure if it's on by default.

@ghost
Copy link

ghost commented Mar 22, 2025

For map, this thing (suggested earlier) works with no changes to the language

extension MapKVIterable<K, V> on Map<K, V> {
  Iterable<(K key, V value)> get pairs => entries.map((e) => (e.key, e.value));
}
final Map<String, ({double lat, double long})> map = {
'Chicago': (lat: 41.878113, long: -87.629799),
'London': (lat: 51.507351, long: -0.127758),
'Aarhus': (lat: 56.162937, long: 10.203921),
};
main() {
  for (var (city, (:lat, :long)) in map.pairs) {
    print('$city $lat $long');
  }
}

This is the only syntax that 1) fits into a zoo of existing syntax forms 2) works
The method "pairs" has to come out of the box.

@Reprevise
Copy link

Related: dart-lang/sdk#54965

@ghost
Copy link

ghost commented Mar 22, 2025

I don't understand this argument from the thread^:

I don't want to add such a getter to maps. Map entries are not anonymous pairs, maps are not just sets of pairs. The key and value roles are significant.

In any record, the roles of components are significant. E.g., we can encode coordinates (x, t) in the record where x is a distance, t is time, their roles cannot be interchanged. But (x, t) syntax is convenient nevertheless.

@TekExplorer
Copy link

TekExplorer commented Mar 22, 2025

You know, that extension could be added to the collection package, much like we have many other useful convenience methods presented, without needing to mess with the SDK any

@ghost
Copy link

ghost commented Mar 22, 2025

It won't be "added" until somebody adds it. (try submitting a patch and see how it goes?)

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
Projects
None yet
Development

No branches or pull requests

7 participants