Currently the following method is suggested:
abstract class Record {
static Map<Symbol, Object?> namedFields(Record record);
}
One downside of using Symbols here is that you cannot safely get the String value that they represent without using mirrors. This means you can't use this method on its own to serialize a Record in a useful way as the Symbol is not a serializable object and can't be converted to one on all platforms.
Using Strings does potentially come at a code size cost to the runtimes, so there would be a tradeoff here. It may even be useful to provide both options to avoid the cost of the String representations when not needed (for instance if this was implemented as an extension method as in 1275 then the compiler could retain only the strings for the record types on which this getter was used)?
Yeah, I did consider using String (because, honestly, who even knows what the heck a Symbol is in Dart?). But that felt really inconsistent to me given that Function.apply(), which is sort of the dual to this method, uses Symbol.
I'd be all for removing Symbol if it wasn't a big breaking change and would probably cause code-bloat for AoT compiling.
For now, symbols is how you refer to source names/identifiers at run-time in Dart.
I also think the namedFields function should be in dart:mirrors to begin with. Either you know the type, or you can't access members except using dynamic. Introspection belongs in dart:mirrors.
I'd be all for removing Symbol if it wasn't a big breaking change and would probably cause code-bloat for AoT compiling
Could you please elaborate on that? I thought symbols were introduced for reasons related solely to minification. And minification makes a difference mostly in the context of web apps (affects download size and time). Where does AoT come into play in this picture?
When Symbol was introduced, web compilation was our only ahead-of-time compilers.
Properly used symbols allow the AoT compilers to know (better) which names you need to retain at run-time, so it doesn't have to retain all source names as strings. Minification was the first place where that became significant, because it needed an explicit translation table for between minified names and source names for noSuchMethod to work (someone doing if (invocation.memberName == "foo") ... needs the un-minified source name of the member). The effect applies to all AoT compilation, though. You can avoid keeping source name strings in the program entirely for some names, but only if you know which source names you're actually going to be checking against in noSuchMethod. Using symbol literals makes those names obvious in the source. (There's more to it, including analysis of which method signatures might hit noSuchMethod, because that's another way to create symbols, but it all helps).
As an experiment, I compiled a small dart program (1300 lines, imports math and typed_data only) using dart2native on Windows, got 5MB .exe file. How much bigger would it become had it retained all identifiers? Let's assume generously there're 10000 unique identifiers in the source code (including imports) with an average size of 20 characters, we will get 200K, which amounts to 4% in terms of program size. Not sure the difference is dramatic enough to justify the feature.
It is nice that the return values of these methods can be passed directly to Function.apply, which does enable some nice functionality potentially, but I only see that truly being useful if you can also go from an untyped Map to a Record, which will require the String names of the named fields to exist in the program.
I do think it would be a good time to evaluate the cost of String representations for these names and see if it isn't something that we should be using in new apis.
I don't think going from an untyped map to a statically unknown tuple type should be possible, not any more than creating an instance of class that you don't know at compile-time that you want to instantiate.
A program should be able to tell, ahead of time, which tuple structures actually exist in the program by looking at the tuple construction expressions. If no expression exists of the shape (x: 42), then the tuple type ({int x}) simply does not exist for the program.
If you know the shape, then you don't care whether it's symbols or strings in the map, you are just going to write
if (input["type"] == 4) {
var data = input["data"];
return (data["0"], data["1"], x: data["x"]);
}
(I also think Function.apply should be moved to package:mirrors).
I don't think going from an untyped map to a statically unknown tuple type should be possible, not any more than creating an instance of class that you don't know at compile-time that you want to instantiate.
Ya I think that makes sense, it wouldn't have to be statically unknowable though.
For instance, imagine if there was a way to easily get from a Function type, a static record type with the same shape as that function types arguments.
And then if you could create an instance of that record from a Map + static record type, and pass that to Function.apply, we could have a potentially very low boilerplate way of de-serializing JSON into classes (or records). We would also want constructor tearoffs though.
There are a lot of ifs and this isn't a fleshed out proposal, but it would be nice to see if there is something there and not totally close the door on it (yet).
There would be no need to ever use Function.apply if we are allowed to write this instead:
var tuple=(1, "Hello", {x: 1});
myFunction(...tuple);
Right, that is essentially the same effect as Function.apply though and would also use the symbol representations of the named arguments afaik.
Spread arguments is very different from Function.apply in that it is statically type-checkable.
If you have
int foo(int x, {String y}) => ...;
and
var args = (42, y: "no");
then I'd expect foo(...args) to check the type and structure of the arguments against the foo function type, and for the invocation to have type int.
Doing Function.apply(foo, Record.positionalElements(args), Record.namedElements(args)) will have no static typing and a type of dynamic.
So, very much not the same, the ...args is basically destructuring of the tuple into the arguments list, like any other destructuring, then then doing a completely normal function call, not a reflective invocation. There are no symbols involved at all, all name matching is done at compile-time.
That also means that there is something it can't do, which Function.apply can do: Call a function with a number of arguments which isn't known at compile-time.
Most helpful comment
Spread arguments is very different from
Function.applyin that it is statically type-checkable.If you have
and
then I'd expect
foo(...args)to check the type and structure of the arguments against thefoofunction type, and for the invocation to have typeint.Doing
Function.apply(foo, Record.positionalElements(args), Record.namedElements(args))will have no static typing and a type ofdynamic.So, very much not the same, the
...argsis basically destructuring of the tuple into the arguments list, like any other destructuring, then then doing a completely normal function call, not a reflective invocation. There are no symbols involved at all, all name matching is done at compile-time.That also means that there is something it can't do, which
Function.applycan do: Call a function with a number of arguments which isn't known at compile-time.