Open mateusfccp opened 1 month ago
I like this. It's a logical extension of having a default values in functions, and i can already see many situations where it could be useful.
The syntax of T default D
is okay, nothing special and easy to understand, but it's also quite long. It could definitely work, without introducing many reading problems for the developper, or parsing problems for the compiler.
But since it's quite long, it means the class definition could become too long with this addition, so much so, that it could hurt the readibility of the declaration. And so, i would like to propose a sort of C++
-like syntax of T(D)
, which is equivalent to T default D
or T = D
. This syntax is a lot shorter than T default D
, it also doesn't have as much ambiguity as T=D
, when used in combination with extends
etc.
class Foo<T, U(D)>
class Foo<T, U(D), V(E)>
class Foo<T, ValueT(int) extends num>
What needs to be solved right now, other than the exact behaviour in code, is the LSP support, type inferring and syntax highlighting.
This syntax needs to have a good LSP support for quickly catching type errors, inferring the actual type, inferring when the developer needs to specify a type, when the default type and the type it extends are incompatible and so on.
The syntax highlighting is important, because it can effectively prevent some readability issues from happening, without modifying the syntax. The syntax highlighting issue is about differentiating the generic type from the default type - when looking at the class definition, the generic type should be the most eye catching. The default type instead should be less visible than the generic type, and should instead serve as a hint for the developper. It's a difference between ValueT(int) and ValueT(int). The default type should be less eyecatching because, even though the type has a default value, it's still generic and the developper should still be counting on it being generic. It also prevents the earlier issue with T = D
being ambiguous, since the developer would be more focused on the T
, rather than both types having the same importance.
*Imagine the bold and italic is a syntax highlighting
The syntax of
T default D
is okay, nothing special and easy to understand, but it's also quite long. It could definitely work, without introducing many reading problems for the developper, or parsing problems for the compiler.But since it's quite long, it means the class definition could become too long with this addition, so much so, that it could hurt the readibility of the declaration. And so, i would like to propose a sort of
C++
-like syntax ofT(D)
, which is equivalent toT default D
orT = D
. This syntax is a lot shorter thanT default D
, it also doesn't have as much ambiguity asT=D
, when used in combination withextends
etc.
This is a possibility. I agree that default
is very verbose. Statically wise, however, I think it makes sense.
For instance, let's consider someone who never read a Dart code before, reading both of these:
// 1
class Foo<T default int> {}
// 2
class Foo<T(int)> {}
In my opinion, even if one doesn't know Dart well, it will be mostly clear what (1) means, but I couldn't tell the same for (2).
I'm going to add your suggestion as an alternative in the proposal, though.
What needs to be solved right now, other than the exact behaviour in code, is the LSP support, type inferring and syntax highlighting.
Although those are important matters, I think they matter more once (if) the proposal is refined and accepted.
For now, I think we should think about the implications (I surely missed some of them) of these changes and how we can solve it properly (if they are even solvable with this approach).
In my opinion, even if one doesn't know Dart well, it will be mostly clear what (1) means, but I couldn't tell the same for (2).
People don't do generics if they are beginners in a language or programming. Also, the problem about the syntax being more difficult to understand for beginners, could be easily mitigated by 3 minutes google search.
Regardless, i was talking about syntax highlighting, because i think it can solve the issues with ambiguity, without changing the syntax (whichever syntax is gonna be implemented). I saw it as a possible solution, more than as a nice thing to have.
I agree that the implications should be studied first, rather than the developer experience, but that's not my area of expertise, since i don't do generics very often (generally an interface will suffice).
People don't do generics if they are beginners in a language or programming.
I don't agree. People who are beginners in programming in general may not use generic, but people who come to Dart with a programming background, will certainly use generics. It's not an advanced feature or anything.
Also, the problem about the syntax being more difficult to understand for beginners, could be easily mitigated by 3 minutes google search.
This is true, and we could expand it to every syntax of the language, but I still think the team values easiness of adoption.
I'm not against cryptic syntaxes myself, but as far as I know, one of the goals of Dart is to be as accessible as possible, so I'm trying to align the proposal with the overall language objective.
I like the idea of allowing an explicit default generic type. It's very useful once in a lifetime.
What should be inferred for the following class?
class Foo<T> { // T extends ???
final T bar;
Foo.baz([this.bar = 42]);
Foo.qux([this.bar = "42"]);
}
If the generic type was implicit, as which option would T in Foo([this.bar = 42]);
be inferred?
class Foo<T extends dynamic> {} // 1, current behavior and compile-time error.
class Foo<T extends int> {} // 2, breaking change and possibly error-prone.
class Foo<T = int> {} // 3, compile-time error.
class Foo<T extends int = int> // 4, breaking change and possibly error-prone.
// ??? 5
@Wdestroier
This is one of the reasons why I avoided dealing with inference in my proposal.
IMO, we shouldn't infer the default value based on the constructor.
But, if we do, for your case where we have many constructors, we would have to do LUB between all arguments that has static type T
. In this case, between int
and String
, which would result in Object
.
IMO this does not scale well, so I still think that we should simply not infer the default value of a type parameter ever.
By having a default type parameter value, we can accept default values in the constructor where …
Not really. A default constructor parameter value isn't necessarily sound just because the type argument can be omitted. It needs it to be omitted, otherwise you can write Foo<Never>()
and presumably get the default int
value as argument.
That invocation needs to be invalid, while also allowing Foo<int>()
to be valid.
That's a different feature: optionally optional parameters, parameters which can only be omitted if their default value is valid for the actual parameter type. I'm pretty sure I specified something like that before.
One of the consequences of that is that it must be visible in function types too.
Something like Foo Function([T arg = int])
, meaning a parameter which can be omitted if it's type accepts int
.
This is a possible solution for #283 and other similar issues.
This is the first time writing a slightly formal proposal, so please be patient and let's work together to improve this.
I may not have considered potential issues, and my language is certainly not as precise as it should be, but I think the idea can be understood.
Overview
This proposal suggests that we allow for a type parameter
T
on a class to have a default value.With this proposal, we could do the following:
Motivations
Adding or removing type parameters from a class is inconvenient (https://github.com/dart-lang/language/issues/283)
Consider the following cases:
Case 1: Changing the number of type paramters of a class that already has type parameters
Then, adding a new type parameter to
Foo
is a breaking change:Case 2: If the client has
strict-raw-types
enabled, adding a type parameter to a class that has no type parameterThen, adding a new type parameter to
Foo
is a breaking change:Known workarounds
Using
typedef
For some cases, using
typedef
may be a valid workaround.Consider the following case.
Instead of simply adding a new type parameter to
Foo
, we provide another class andFoo
as atypedef
.However, this is not always desirable, because if we want
U
to be used by the client, it will have to refer to the new class:Having a default value, in this case, wouldn't be less breaking. It would still be breaking for
Baz
to support theU
parameter. However, it would be done without the need of two different classes.Allowing for default values in constructors where the field type is a generic type
Currently, the following is an error:
By having a default type parameter value, we can accept default values in the constructor where
T
is expected, as long as the type has typeU
andU <: D
, beingD
the default type forT
.Reduce "clutter" for common cases
This is more than nothing a way of making some codes terser.
For intance, consider the following library code:
Now, we don't know how the clients are going to use library, but we know that the common case is to use
DefaultTag
as parameter. The client can introduce their ownTag
s, but it's an exceptional case that's not commonly used.By providing
DefaultTag
as the default value forT
, the majority of the clients can extend from the raw typeA
, while the exceptional cases can specify their customTag
.Syntax
Considering that we are dealing exclusively with classes[^1], the grammar for class declaration would be changed in the following way:
[^1]: I think it is possible to extend this to non-classes too, like regular functions methods, but it's out of the scope of this proposal. If we do, we don't have to split
typeParameter
intoclassTypeParameter
.This would allow us to specify the type parameter default value with the following syntax:
Alternative syntax
Using
=
instead ofdefault
For default values in a parameter,
=
is used.We could use
=
also for default type parameter values.The syntax is terser.
However, one^2 could argue that it's syntatically confusing when used with
extends
.In the first case,
T extends num = int
may give the idea that we are equallingnum
toint
. In the second case,T = int extends num
may give the idea thatint
extendsnum
(which is not entirely false, but in the context we want to mean thatT
extends num).There are other shorter keywords that could be used, but I don't think any of them is semantically clearer than
default
.Using parenthesis instead of
default
As suggested by @hydro63, an alternative would be to use parenthesis, in the following way:
This approach has the following pros:
=
;=
when used withextends
.The con, however, in my opinion, is that it does not conveys the semantics as good as
default
or=
.Someone who is just starting with Dart may understand what
T default int
means, but will hardly understand whatT(int)
means without reading the documentation.Semantics
T
be a type parameter of a classC
with default valueV
:T
has a boundU
andV <: U
[^3] does not hold;c
inC
, any parameter of typeT
inc
can have a default valuev
iffv
is constant andv
has a static typeV2
such thatV2 <: V
[^3];C
is used as a raw type:T
has no bound, the instantiation to bound algotithm should inferT
to beV
instead ofdynamic
;T
has a boundU
, the instantiation to bound algotithm should inferT
to beV
instead ofU
.[^3]: These semantics may be changed if we have statically checked declaration-site variance. For instance, if the type parameter
T
is contravariant with a default valueU
, we may want to guarantee thatT <: U
.Other questions
Should default type parameter value be inferred from constructor?
Consider this same code as before:
With the proposed changes above, it still wouldn't work, unless we specified
T default int
.We can infer
default int
from the default parameter of the constructor.If we have more than one parameter, like:
Then we could apply the LUB algorithm. In this case, it would be inferred as
T default num
.My personal opinion is that we shouldn't do this, and I would rather prefer for a default value for the type parameter to be always explicitly stated.