Switching on bounded generic types
It appears exhaustiveness checking isn't performed when switching on bounded generic types.
sealed class A {
abstract final String s;
}
class B extends A {
final String s = "B";
}
class C extends A {
final String s = "C";
}
// Error: The type 'Type' is not exhaustively matched by the switch cases since it doesn't match 'Type()'.
// Try adding a wildcard pattern or cases that match 'Type()'.
String getString<T extends A>() => switch(T) {
const (A) => throw UnimplementedError,
const (B) => B().s,
const (C) => C().s,
}
By adding a default case to this (which I don't can ever be called) the error goes away and this behaves as expected. I may have missed the proper way to do this. If I had an object of type T then I understand I could use the usual exhaustiveness checking (Matching on B _ etc).
As an aside, matching const nullable types seems to require typedef'ing them, e.g.
typedef AorNull = A?;
switch(T) {
const (A?) => , // Error: Expected an identifier
const (AorNull) => , // Analyser is happy
}
Thanks for your help 🙂
You're switching on Type objects, which are not an exhaustive type.
Even if Dart had the ability to recognize all the possible Type objects that could possibly be values for the types bound to T, it's non-trivial to exhaust them if any type is generic, because Type object comparison is only by equality.
And in the current case, where the types are not generic, you haven't checked for Never.
So, unlikely to become a feature. The way to write generic methods is to treat the type parameters generically. Switching on them is closer to reflection than generics.
Yep that makes sense 🙂
Could you explain what you mean by trusting type parameters "genetically"?
Is there a 'proper' way to do something like
class FromJsonObject {
static FromJsonObject fromJson(json) => ...;
}
class Subclass1 extends FromJsonObject {
static Subclass1 fromJson(json) => ...;
}
class Subclass2 extends FromJsonObject {
static Subclass2 fromJson(json) => ...;
}
// switch expression here is pseudocode
T fromJson<T extends FromJsonObject>(Map<String, dynamic> json) {
return switch(T) {
Subclass1 => Subclass1.fromJson(json),
Subclass2 => Subclass1.fromJson(json),
FromJsonObject => FromJsonObject.fromJson(json),
_ => throw UnimplementedError(),
}
}
Essentially something like static inheritance - or maybe this would be considered the wrong way to go about such a thing?
Typically I have this sort of thing pop up when constructing model classes from some serialised format, where the model classes inherit some (often sealed) base class.
Thanks again for your help
Could you explain what you mean by trusting type parameters "genetically"?
I mean "generically". Damn mobile keyboard autocorrect. Fixed!
Is there a 'proper' way to do something like ... Essentially something like static inheritance
No. You cannot go from type parameter, or the even less useful Type object, to static members. It's safer not to try.
If you knew the actual static type at the point where you call fromJson, you could just do ThatType.fromJson(map), so I'm guessing you only have a type parameter for it. And then you have already lost.
That type parameter should be followed, from start to end, by an object that allows you to create an object of that type from a JSON-like map. Maybe just the T Function(Map<String, Object?>) function that is the fromJson function of the type.
This is where static interfaces would be useful. Rust just... does this. ie: <T extends FromJson>() => T::fromJson(map)
Sadly, we're stuck doing <T>(T Function(Map<String, dynamic>) fromJson) => fromJson(map)
Rust just... does this.
"Just" is doing a lot of work in this sentence. :)
Rust compiles generic code using monomorphization: you get a full separate copy of the compiled code for every generic method for every set of type arguments that it gets instantiated with. This is one of the reasons why Rust has famously long compile times, and leads to large code size. The latter is an important issue for a language like Dart which is compiled to JavaScript and also used on mobile devices where download size can have a real impact on software success.
Rust can easily let you call "static" methods on type parameters because at the point in time that the function is compiled, that type parameter has been replaced with a fully concrete type. Rust can look up the function on that type and then statically dispatch to it. And it will do that... for every single instantiation of that function.
Dart doesn't monomorphize, which means being able to call static functions on a type parameter requires some different dispatch mechanism. Dart does reify type parameters at runtime, so we could potentially support virtual dispatch of static methods on type parameters at runtime. We've discussed that off and on for years. But it would be a pretty large, complex, feature and it's not clear if the benefits are worth the costs.
Sounds like its just... static interfaces...?
which would probably have to be virtually dispatched anyway, but for the sake of much improved DX and not needing to provide factories when the generic I'm passing should already provide this information then I think its worth it.
if passing a type into a T decode<T extends static FromJson>(String source) is akin to passing in decode<Data>(str, Data.FromJson)(or a normal parameter that's hidden, or maybe the type just... contains the pointer itself) then great. good enough. acceptable.
I just think that types as they are right now are almost completely useless beyond the absolute basics, and that's a damn shame.
just the fact that people expect to do T.fromJson intuitively and cant should be indication on its own that something is missing