dart-lang / dart-lang/language
Implicit coercion through implicit constructors.
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
Dart has implicit coercion only in very few cases: Implicit `call`-tearoff, implicit generic instantiation, and (arguably) implicit downcast from `dynamic` (which doesn't coerce anything, the value doesn't change).
A fairly common pattern, especially when interacting with native JS code, is an argument like `dynamic location /* String or Uri */` where the same function can take its URI argument as either a `Uri` object or a `String`. Dart does not have union types, and the author didn't want to have two different functions (which can explode combinatorially if there is more than one such parameter), so the only choice was to choose a supertype, and do type testing at runtime.
An alternative would be to introduce a way for a `String` to be automatically convertible to a `Uri`. Maybe only locally when using this API, or generally.
The [proposal](https://github.com/dart-lang/language/blob/main/working/0107%20-%20implicit-constructors/feature-specification.md) suggests introducing *implicit constructors* only through static extensions.
An implicit constructor is a constructor marked with the modifier `implicit`, which must be callable with a single positional argument.
At a point where a the static type, `S`, of an expression is not otherwise assignable to its context type, `Q`, and it needs to be, the *context type* is checked for implicit constructors that would accept the expression's static type.
We look for *allowed type declarations* of the context type (greatest closure of the context type scheme) as follows:
* If the context type is `FutureOr`, the allowed type declarations is the union of the allowed type declarations of `Future` and of `V`.
* If the context type is `V?`, the allowed type declarations is the union of the allowed type declarations of `Null` and of `V`.
* If the context type is a type variable `X` with bound `B`, the allowed type declarations are the allowed type declarations of `B`.
* If the context type is a promoted type variable `X&B`, the allowed type declarations is the union of the allowed type declarations of `B` and of `X`.
* If the context type is the type of a sealed class declaration *C*, the allowed type declarations is the union of the set {*C*} and the allowed type declarations of its immediate subtypes.
* Otherwise if the context type is the type of a `class`, `mixin`, `mixin class`, `enum` or `extension type` declaration *D*, the allowed subtypes is the union of the set {*D*} and the set of the allowed type declarations of all sub-classes/extension types of *D* that are declared in, or imported into, the current library. ("Imported into" means in the export scope of an imported library, whether with or without prefix, and not hidden by a `show` or `hide` clause, even if the name has been conflicted or shadowed.)
Then find applicable types and constructors
* For each declaration allowed type declaration `D`:
* If the the declaration is accessible from *L*,
* For each implicit constructor declaration of `D` and each available implicit static extension constructor of `D`, `D.id` where `id` is `new` for the unnamed constructor:
* If `D.id` is accessible from *L* (`id` is not private to another library),
* and we can infer type arguments `` to `D` (if generic) as we would for `T _ = D.id(e)` where `e` has the static type `S`,
s.t. `D` is well-bounded and a subtype of `T`,
* then we consider the type `D` and constructor name `D.id` as an _applicable_ type and constructor pair.
Now look only at the applicable types and constructor pairs.
One type and constructor pair is _more specific than_ another if:
* The type of the former is a subtype of the latter, and not vice-versa.
* Or otherwise (neither type is a subtype of the other, or both are subtypes of each other)
* The first positional parameter type of the former constructor (instantiated at its type as `D.id`) is a subtype of the
first positional parameter type of the latter constructor (also instantiated at its type), and not vice versa.
* Or otherwise (neither first positional parameter of the instantiated types is a subtype of the other, or both are subtypes of each other)
* The first positional parameter type of the former constructor when the type is instantiated to bounds, is a subtype of
the the first positional parameter type of the latter constructor when its type is instantiated to bounds, and not vice versa.
If there is precisely one applicable type and constructor pair among the applicable ones that has precedence over *all other* applicable type and constructor pairs, then the original expression is implicitly coerced by using its value as the argument to the most specific `D.id`, producing a value with static type `D` which is a subtype of `T` and therefore valid.
Otherwise a compile-time error occurs, because `S` is not assignable to `T` as required.
We say that a type `S` is coercible to `T`, in a specific lexical context and library, if `S` is assignable, or if there is a single allowed, applicable and most specific implicit constructor which can coerce `S` to `T`.
The rules for which sub-classes to look for constructors in uses the [ideas of `.enumValue` shorthands](https://gist.github.com/lrhn/0c867668ba76797ffab2eff5726fe6bc) to look for subclasses that are readily available, or implicitly implied by a sealed class. That makes sense because both are looking for a declaration which can provide a value for a context type, which means looking for "reasonably available" subtypes of the context type.
If "enum value shorthands" are extended to also work with constructors (it should), and we also get this feature, then you can write either `= e` for an implicit constructor invocation. or `.id(e)` for an explicit short-handed constructor invocation, and get the same result. (That's why the two features should use the same rules.)
The "most specific" discrimination tries to follow the pattern of `extension` resolution:
* The most specific result type is best.
* If not, the most specific argument type is presumed to best know how to treat that argument.
* If not, a specific type parameter type is considered more precise than a generic type that happens to be instantiated to the same type.
If we get implicit static extension constructors, then it'll probably be important to use the same discrimination rules (although I'm not sure they're applied to the same types).
All these rules can be tweaked.
Contributor guide
Research direction
Start with the linked working/0107 - implicit-constructors/feature-specification.md proposal and compare it with the issue's coercion, allowed-type, applicability, and specificity rules. The issue does not identify implementation files or tests; completion would require resolving the design and producing an accepted language specification for implicit constructors.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 20/100