dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

Static type metadata guided shorthands and features #3905

Open lrhn opened 2 months ago

lrhn commented 2 months ago

This is more of a meta-issue around the use of static information, some that is not directly part of a type, used to infer more than just types.

There are multiple features that try to rely on a static type, sometimes the context type, to allow shorthands or other features, and more features that probably ould do so.

These all rely on the static type of something, and sometimes on metadata related to that static type.

Those are all potentially useful features, but are not really viable in Dart without formally defining the flow of such metadata through type inference and type operations (UP or similar).

If this is something we want to do, we'll have to do that work on propagating the metadata (parameter names, parameter default scopes here, possibly other things too in the future). If we do that work for one of these features, it may enable some of the other features.

We've generally shied away from giving semantics to positional parameter names outside of their scope, because doing so makes it a breaking change to rename a parameter, which it currently isn't. We'd have to accept that change if we want to use the names, or allow a function to opt in to its names being public - but if we expect most/all positional parameters to be public in the future, it might be better to just opt out legacy code, and have people do their final renaming before upgrading to the language version which changes it.

So, at a higher level, is this kind of "static metadata" based programming shorthands or aides something we want at all? And if so, how far are we willing to go?

(One can see dynamic and void as similar "metadata" on a type that is really Object?. We're treating them as separate types, which works because the information follows the type, and we've had to define things like MORE_TOP to account for the static difference - that is, we've modified our type operations to preserve and combine this particular kind of metadata. So there is precedence.)

tatumizer commented 2 months ago

Finding good names for positional parameters is not always possible - especially for callbacks. E.g. the parameter of Iterable map declared in Iterable<T> map<T>( T toElement( E e ) ) is named "e", which is no better than $1; no matter what other name you choose as a replacement for e in this declaration, it cannot be specific enough to capture the meaning - so will become a version of $1, just with a less predictable identifier.

When we use the current syntax, we can assign a more semantic name like listOfRabbits.map((rabbit)->rabbit.age)), or maybe something shorter like listOfRabbits.map((r)->r.age)), but listOfRabbits.map((e)->e.age)) is hardly the best candidate. In any case, using $1 won't cause more problems than e.

The same problem of finding a good name can be encountered across dart APIs. The parameter of sqrt is called x; you can try to rename it into arg or anything - and it won't be better than $1. (Interesting, but trivial, observation: when we pass parameters into direct calls, the semantic name in the program translates to a meaningless name inside a function: in pow(rabbitPopulation, 10) : "rabbitPopulation" becomes "x" inside "pow"; in callbacks, we see the reverse: meaningless name can acquire meaning).

I find a bit strange that after introducing records, dart hasn't allowed the names like $n also to refer to positional parameters. This would be rarely needed, but would still solve the problem of passing parameters conditionally:

foo( 
  $1: if (cond) 42,
  $2: "hello"
);
tatumizer commented 2 months ago

(Cont-d) A much bigger problem IMO is that the proposed syntax, no matter if we write => a + b or => $1 + $2, does not scale. If we want to insert a print, we need to fully refactor - because the syntax is designed for a single-expression body.

foo(=>a + b);
// becomes
foo((a, b) { print(a); return a+b; }

These expressions have nothing in common, thus breaking the principle of syntax similarity/dissimilarity between similar/dissimilar "things". The current syntax doesn't scale well either, but

foo((a, b)=> a+b);
foo((a, b) { return a + b; });

at least have a common (a, b) part. With the new syntax, any similarity is gone - the two expressions look like they came from two different worlds. As an alternative, consider the syntax :{ $1 + $2 } (I mentioned it in the parallel thread). It unifies both forms and can be back-ported to the existing syntax:

mylist.map(:{ print($1); $1*$1 });
rabbits.map((rabbit) { print(rabbit); rabbit.age }; // no semicolon after the last expression, no explicit "return"
// plus evaluation syntax
foo(${ print("in anonymous function"); 2+2 });
// as a replacement for IIFE
foo((){ print("in anonymous function"); return 2+2; }());

Nitpick: I browsed the dart core API trying to discern the naming conventions for positional parameters in current libraries. One thing that baffled me a bit was this declaration: double sin(num radians). Today, no one cares, but if you make the names "official", it's better to avoid this word "radians". sin(x) and its friends are defined for real numbers, otherwise we get cos(radians: sin( radians: pi/2)), which is absurd. I checked other languages (java, C#, python) - they call the parameter x or a, and mention radians only in the description. (the parameter names in math functions are not very consistent in respective libraries: sometimes it's "a", sometimes "x", or "d", but always a single-letter identifier). It's not that easy to come up with good names after all!

lrhn commented 2 months ago

My main worry about making names of positional parameters significant, is that existing code wasn't written for it, and after the feature has launched, changing those names is a breaking change.

The toElement(e) is a good example, it should probably have been convert(element), to be consistent with other declarations. But it had never mattered, so it was never fixed.

If function literal shorthand uses the names of the context type, like a callback parameter, that's a position where people have so far been even less inclined to consider the names than they would in directly callable APIs, after all they're the only one who'll call the function. If they have a name at all. Falling back on \$1…\$n if the parameters have no name is probably fine. (Not going to discuss the precise syntax for function literal shorthands here, that has its own issue.)

IMO: The arguments to sin and cos are radians, so if the code looks weird if you write that out, maybe it's the code that's weird - why it's someone passing the output of sin, a number in the range -1..1, into cos which expects a number in the range -pi..pi? Single argument functions aren't the target audience for named positional arguments, but this one actually doesn't worry me. It's not like cos(x: sin (x: pi/2)) is easier to read, it's just shorter, but omitting the x: is even shorter.

tatumizer commented 2 months ago

I have to disagree on radians, but this is beside the point here (it can be a subject for a philosophical discussion). A bigger issue is that the feature will necessitate a fair amount of change across dart APIs without clear justification.

For normal functions (not callbacks) it's all about conditional parameters, which are rarely needed, and even then, $n syntax achieves the same with no modifications to the existing code.

For callbacks, you will have variable names falling from the sky. When we denote the parameters as $n, it's at least clear that they are the parameters. If $1 becomes an element - the name comes from nowhere, may conflict with something in your code, too long for the occasion, etc. If you estimate cost/benefit ratio, it won't work in favor of the feature :-)

(As for =>expr notation, you will probably have to admit that it goes against your own syntax/semantics principles:-)