Description
This is a strawman proposal to address (some of) issue #357.
Here is an example of it in use. Say you have a couple of data types:
enum ButtonAlignment { top, center, bottom }
class Color {
final int red, green, blue;
const Color(this.red, this.green, this.blue);
}
You also have this separate namespace-like class for working with colors:
class Colors {
static const red = Color(255, 0, 0);
static const yellow = Color(255, 255, 0);
static const green = Color(0, 255, 0);
static const cyan = Color(0, 255, 255);
static const blue = Color(0, 0, 255);
static const magenta = Color(255, 0, 255);
static Color darker(Color color) =>
Color(color.red ~/ 2, color.green ~/ 2, color.blue ~/ 2);
}
Then you have a class that uses these:
class Button {
Button(
this.text, {
this.alignment,
this.color,
});
final String text;
final ButtonAlignment alignment;
final Color color;
}
To create an instance of this class today, you have to write:
Button('One',
alignment: ButtonAlignment.top,
color: Colors.darken(Colors.blue)
);
You would like to be able to write:
Button('One', alignment: top, color: darken(blue));
This strawman enables that. But to turn it on, you need to change the Button and Colors classes like so:
class Button {
Button(
this.text, {
this.alignment from ButtonAlignment, // <--
this.color from Colors, // <--
});
final String text;
final ButtonAlignment alignment;
final Color color;
}
class Colors {
// ...
static Color darken(Color color from Colors) => // <--
Color(color.red ~/ 2, color.green ~/ 2, color.blue ~/ 2);
}
The from
clauses after the marked parameters are how those parameters opt in to special lookup rules for bare identifiers in arguments. When evaluating an argument expression:
- If the argument is an identifier or a named function call,
- and the identifier cannot be resolved in the scope where the argument expression appears,
- and there is a
from
clause on the parameter, then - treat the argument expression as a static getter or method call on the referenced type.
The from
clause lets an API author deliberately opt in to a set of identifiers that become valid arguments for that parameter. An API author can sort of say "here is the enumerated set of short names this parameter accepts".
Pros
Compared to other proposals, this strawman more explicit and verbose. That explicitness provides a couple of benefits:
Identifiers can be looked up on another type
One of the common areas where users are frustrated by redundancy is color parameters in Flutter:
var myStyle = TextStyle(Color: Colors.red);
But, as you can see here, red
isn't a property on the actual Color class, it's a constant on a separate Colors class. An explicit from
clause lets an API deliberately redirect to a separate type like that, like the example here shows.
The namespace can be custom-tailored to the API
In fact, API authors can define their own custom namespace-like classes containing exactly the identifiers they want for a specific parameter. Any given parameter can have its own little purpose-built autocomplete namespace.
For example, you could do:
class Ascii {
static const a = 97;
// ...
static const z = 122;
}
class Text {
Text.fromCharCode(int charCode from Ascii) ...
}
Here, the parameter's actual type is int
, which isn't specific to any particular domain. There's no way we're going to add the ASCII table to the int class itself in order to have nicer looking charCode arguments.
But since the author of a parameter chooses which type to look up argument shorthands on, this fromCharCode()
constructor can point to a type specific to the API's domain.
APIs are less fragile
The API author knows that changing the from
clause can be a breaking change to uses of the API. If we rely on the parameter's type to determine which identifiers are allowed, then changing a parameter type is always a breaking change, even in ways that aren't breaking today.
For example, say we decide that Button should also allow a string of CSS for its color. That means loosening the type of color
to both Color or String:
class Button {
Button(
this.text, {
this.alignment,
this.color,
});
final String text;
final ButtonAlignment alignment;
final Object color;
}
If the parameter's type determined what identifiers could be used, every existing callsite could have just broken. But by specifying the type that identifiers are looked up on explicitly, the API author can loosen the type while still preserving the lookup on Colors:
class Button {
Button(
this.text, {
this.alignment,
this.color from Colors,
});
final String text;
final ButtonAlignment alignment;
final Object color;
}
The parameter's type has changed, but every existing callsite relying on lookup on Colors continues to work. By not making the identifier lookup implicit based on the parameter's type, we give API authors more freedom to change parameter types without breaking users. They can evolve the parameter's type and its lookup namespace independently.
Cons
There are some negatives, though:
-
The syntax is strange and somewhat verbose. We've already packed quite a lot into the parameter list grammar, and this adds even more.
-
The benefit only applies to APIs that have opted in. If we roll out this language feature, it doesn't help any API users until the API maintainers have taken the time to update their parameter lists to take advantage of it.
-
The syntax only helps arguments. Other proposals based on context type can apply in any expression position where a context type may be available, like assignments, collection literals, etc.
We could fairly easily extend this strawman to support the right-hand side of binary operators (so
==
would work). Other syntactic locations are harder because it becomes less clear what API you should query to figure out what type to look up the names on.