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

Is it possible to match a record to a map? Should it be? #2886

Closed
jodinathan opened this issue Mar 4, 2023 · 6 comments
Closed

Is it possible to match a record to a map? Should it be? #2886

jodinathan opened this issue Mar 4, 2023 · 6 comments
Labels
patterns Issues related to pattern matching. question Further information is requested

Comments

@jodinathan
Copy link

I am doing some tests and it seems that is currently not possible to match a Record against a Map

The following code produces No and true:

import 'dart:convert';

typedef FromMap = ({String foo, int bar});

void main() {
  final decoded = json.decode('{"foo": "yat", "bar": 123}');
  
  if (decoded case {'foo': String foo, 'bar': int bar}) { // matches
    final fromMap = (foo: foo, bar: bar);
    
    print(fromMap is FromMap); // Unnecessary type check, result always true
  }

  if (decoded case FromMap) { // nope, doesn't match
    print('Yes');
  } else {
    print('No'); // enters here
  }
}

Will it be possible to retrieve a Record from a Map?

I can see packages exposing builders that do that and I was wondering if this should be possible from the core.
If you think it through Map matching is very close to it.

@lrhn
Copy link
Member

lrhn commented Mar 4, 2023

No. (To "is it possible". Probably also to "should it".)

Records are not maps, nor vice-versa. Records are more like objects with a special implicit class per shape, and that type is not a map.

You cannot create a record without writing the shape into the source, because if you don't, the compiler won't know the "class" of that record.
Since a map's content is not part of its type, it's not possible to create a record from a map in a sound statically typed way.

You can treat records as objects, and use object patterns, like:

if (decoded case FromMap(:foo)) {
  print("Just the string $foo");
}

but they're not map objects.

Even if

if (decoded case FromMap) { // nope, doesn't match

matched, because it knows a fancy non-type-safe way to extract a record from a map, it wouldn't help.
It can't promote decoded to FromMap, because it's not a subtype.
Maybe doing

if (decoded case FromMap record) { // nope, doesn't match

could work, but then we go back to the "fancy non-type-safe" operation to go from map to record.

Presumably you'd want to use the string corresponding to the record field name to extract a value from the map.
That requires retaining source names. It would be more realistic to expect a Map<Symbol, ...>, because symbols is what corresponds to source names at runtime — but that won't help you with JSON.
And we don't want to retain source names in production code.

@lrhn lrhn transferred this issue from dart-lang/sdk Mar 4, 2023
@lrhn lrhn added question Further information is requested patterns Issues related to pattern matching. labels Mar 4, 2023
@jodinathan
Copy link
Author

yeah I understand that we shouldn't keep source names in production code and it makes sense.

However, we do need them when we do if (decoded case {'foo': String foo, 'bar': int bar}) {}.

So I am wondering that when the compiler traverse the code and find a Map being matched to a Record like in if (map case FromMap obj) {} it would then retain the sources names as it would in a map-matching.

@lrhn
Copy link
Member

lrhn commented Mar 5, 2023

It's definitely possible, but again, matching source names to string values at runtime is two steps out of my comfort zone.
Had it been symbols instead of strings as map keys, it would only have been one step. 😉

A much more direct match would be matching an object to a record type, like:

class Foo {
  int foo = 0;
  String bar = "";
}
typedef FooBar = ({int foo, String bar});
// ...
  if (aFoo case FooBar r) { ... destructure object and build record ... }

It's still mixin two concepts: Destructuring and re-structuring.
Patterns destructure. They allow you to access the individual values.
Combining those values back into a new object, whether a record or something else, is what you do in the case body.

Converting directly from one structure to another, through some implicit connection between their properties, is something I'd at least require an explicit declaration of, not something the compiler should just guess out of naming or types.

@jodinathan
Copy link
Author

matching source names to string values at runtime is two steps out of my comfort zone

the point is that we are already doing it here: if (decoded case {'foo': String foo, 'bar': int bar}) {}

Converting directly from one structure to another, through some implicit connection between their properties, is something I'd at least require an explicit declaration of, not something the compiler should just guess out of naming or types.

I seriously thought it was possible because the syntax between Records and pattern matching are very alike

@munificent
Copy link
Member

So I am wondering that when the compiler traverse the code and find a Map being matched to a Record like in if (map case FromMap obj) {} it would then retain the sources names as it would in a map-matching.

The problem is that the compiler can't tell which records might flow into which map patterns (or vice versa). In general, this kind of dataflow analysis for dead code elimination is really fragile and hard to make work well.

Maps and records are very different constructs. They solve different problems and they have different trade-offs. That's why Dart has both map and record objects, and why it has map and record patterns.

I seriously thought it was possible because the syntax between Records and pattern matching are very alike

The syntax for record types with named fields does look sort of like a map, which is unfortunate. But it's not because records are map-like. It's because we wanted record types to roughly mirror parameter lists, which use { ... } for named fields. If parameter lists didn't use { ... }, then record types wouldn't either.

@munificent
Copy link
Member

I'm going to close this because we don't have any plans to change this part of the language, but it's a good question. Thanks for asking.

@munificent munificent closed this as not planned Won't fix, can't repro, duplicate, stale Mar 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
patterns Issues related to pattern matching. question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants