Description
and incrementally migrating is not easy; it must be done atomically with the type-parameter-increasing change.
This is not a problem when adding the first type paramter:
class C {}
// This is a non-breaking change:
class C<T> {}
// This is also non-breaking:
class C<T, U, V> {}
However, when adding any type parameters beyond the first, the change is breaking for any clients which specify any type arguments:
class C<T> {}
// This is a breaking change:
class C<T, U> {}
// For code such as the following:
new C<int>();
C<int> c;
class D extends C<int> {}
Introducing a second class while renaming the original class is one way to "deprecate" the previous number of type parameters, and expose the "new" number of type parameters:
class C<T> {
T f1;
dynamic f2;
}
// Non-breaking change:
class C_2<T, U> {
T f1;
U f2;
}
@Deprecated("Now with more type parameters! Use C_2!")
class C<T> extends C_2<T, dynamic> {}
You can ship this non-breaking change, announcing the deprecation, allowing clients to migrate, and later ship a breaking change where you remove C
.
The big problem with this is that no one wants to rename classes. If you look at functions, it is a breaking change to introduce or remove required parameters (or remove or rename named parameters), but this is mitigated by making optional parameters available. Or perhaps the purpose of a function has changed enough with the changed parameters that you can offer it as a new function with a new name, and keep the old function around, marking it @Deprecated()
.
Renaming a class seems like a much bigger and unfortunate change. Classes often do not involve more than two words; typically a few nouns, rather than an English predicate expression. "ContainerViewModel" means the Model of a View of a Container, which should not be changed based on the number of type parameters.
Activity
eernstg commentedon Mar 22, 2019
Would the following approach be sufficiently helpful? (It relies on generalized type aliases, but that feature is coming, it is just sitting in the pipeline a bit longer than we thought because of other features with a higher priority).
Before the change we have this (I'll focus on the change from one to two type parameters because that one embodies the harder issue):
Then we change
C
to have two type arguments, and we'd like to have a per-library migration. We rename the library 'useful_stuff.dart' to 'new_useful_stuff.dart' and provide a new 'useful_stuff.dart' library to make the transition easier:Client libraries stay unchanged, but their imports will now refer to the new "bridge" library 'useful_stuff.dart' rather than the library that actually declares
C
.Each client can then make the choice to change its import to `new_useful_stuff.dart' and adjust the code to provide the new type argument in useful ways.
Of course, if the name 'useful_stuff.dart' must remain unchanged then we could also use a new name for the bridge library, and then clients would have to adjust their imports. That trade-off could be OK, because it is nearly a textual operation (a global search and replace from importing 'useful_stuff.dart' to importing 'bridge_useful_stuff.dart').
srawlins commentedon Mar 22, 2019
This definitely offers an alternative to class renaming.
If the class is defined in a
/src/
library which is already only accessed via a publicexport
, then this solution may be more awkward than the class renaming.becomes
eernstg commentedon Mar 22, 2019
Of course, this might not work in practice (if there are several classes with changes like this, and transitioning might not happen all at once, etc), but in the simple case where we only change
C
it seems likefoo_transitional_c.dart
might as well export the same name space asfoo.dart
used to export (that is, thetypedef
'edC
, plus all other things with no changes), and clients would then only need to changeto
When they are ready to update that particular library to use the new
C
they'll switch back tofoo.dart
.Again, the naming could be swapped such that clients don't have to change anything initially, if it has top priority to avoid changes in client code initially, and if it is acceptable to use a new name for the library when the transition is over: So, clients would do nothing at first, but when they are ready to update a library that imports
foo.dart
then they change the import tonew_foo.dart
and fix the code.munificent commentedon Mar 22, 2019
Technically, it can be a problem here too if the first type parameter has a bound that inference can't satisfy, because then using the raw type becomes an error. More generally, for users that enable "strict raw types", adding a type parameter will become an error.
C# handles this like it handles adding parameters to methods: it lets you overload. You can define multiple classes with the same name but different type parameter arity:
caseycrogers commentedon May 11, 2021
I have a related use case for the same requested feature:
You can't have a class with a Type Parameter and a field member with a default value. eg the following throws a compile error because
String
is not typeT
:The desired behavior would be, if the user doesn't specify
bar
,T
takes on the type of the default value. Alternatively, to be more explicit, this could be written as:The only current workaround I'm aware of is to create a default class that extends the base class, which works but is non-ideal as it requires API users to be aware of and use the alternate class if they want to override default behavior:
eernstg commentedon May 12, 2021
It would be possible (and might be useful ;-) to allow a type parameter to have a default value. We'd need to deal with a couple of difficulties, however:
There is a syntactic conflict, because default values are otherwise specified using
'=' <expression>
after the parameter name, and that works rather well for a value parameter because the type (if specified) occurs before the parameter name:foo({int p = 1})
.For a type parameter, the type is specified as a bound. So
X extends num
means thatX
belongs to the set{T | T <: num}
(whereasint p
means thatp
belongs to the setint
), and this means that the location just after the parameter name is already occupied. But we might be able to use a word likedefault
to create the connection:X extends num default int
looks more readable to me thanX extends num = int
orX = int extends num
.So we'd have this:
The missing part is the default value. Default values could be generalized a lot, cf. #140, so we could use a
computeDefaultValue
getter to satisfy the constraint that the chosen value must be typable as anX
:This illustrates that having a default value that does not have the required type for all values of
X
is tricky, but it would be possible to create sort of a solution by doing some case analysis on the value ofX
.I don't see how we could have an actual default value that satisfies all the possible values of the type variable, except for cases where we can use
Never
:There has been a proposal for defining default values for types, #1227, but that's probably not useful when we wish to define suitable default values for a specific parameter. So it looks like you'd end up having the kind of unsafe case analysis that we have in
computeDefaultValue
above.caseycrogers commentedon May 12, 2021
Your analysis/proposal is a lot more expansive than mine-my assumption/use case is that the default value would be used only if the default type had also been used. eg
Foo<num>()
would/should be a compile time error becausedefault_value
is not a num.Foo()
,Foo<String>()
,Foo(bar: 4)
orFoo<num>(bar: 4)
would all be valid. When a user specifiesbar
,T
is inferred by the type ofbar
. To me it'd be intuitive forT
to be inferred from the type of'default_value'
whenbar
is omitted (the first of the two proposals in my comment above).That said, your proposal (assuming default values were first allowed to be non const) sounds like it could be useful . Eg if you're creating a class for storing and manipulating data types, you might have a list of types with supported zero-values and want to allow users to skip specifying an explicit value for any class with a valid zero value. I don't think there's a way to get around the unsafe
computeDefaultValue
code though....Also I definitely agree on
default
over=
, that's much more readable.eernstg commentedon May 12, 2021
It's an interesting idea that the default value is essentially considered to exist only when the typing situation allows it to be used (in all other situations the parameter would be considered to be
required
).It's not trivial to define that approach, however, because many types are only known by an upper bound:
So we can basically only rely on having a default value when we have a lower bound
L
for the value of the type variableX
, and that is just about the same thing as knowing the exact type. It would also work if we introduce declaration site variance andX
is contravariant:So it might actually work quite well together with sound variance. Interesting! ;-)
mateusfccp commentedon Sep 4, 2024
@eernstg I'm considering this as a viable alternative to introduce phantom types to a class without breaking it.
In this case, it wouldn't require any default values, but its usefulness would also be reduced.
Aside from the default values problem, is there any other problem that you see that may arise from this issue?
7 remaining items