dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.28k stars 1.58k forks source link

Expose isSubclassOf #58261

Open Solido opened 2 days ago

Solido commented 2 days ago

Since the new addition of sealed, base and other OOP keywords, dart offers more ways to model real world systems.

When using inheritance it is common pattern in chain of command as an exemple to ask if a class is a subclass of specific parent to switch implementation.

Kotlin expose isSubclassOf, java Class.isAssignableFrom, etc

I'm now facing a case where I need to handle a structure in parallel with the classes declarations to handle hierarchy and it's getting large and error prone to manually manage each new entry per class.

On Flutter Forum user tenhobie suggested this:

bool typeIsSubtypeOf<T1, T2>(T1 _, T2 __) => <T1>[] is List<T2>;

that works on simpler cases only.

When moving away from flutter and trying to model business relying on OP this would be a considerable addition for large code base simplification.

Thank you for your feedbacks.

FMorschel commented 2 days ago

Similar to https://github.com/dart-lang/language/issues/3837 and my answer on StackOverflow:

Just to add up to lrn's answer. You could also do something like:

extension NullableObjectsExtensions<T> on T {
  bool isSubtypeOf<S>() => <T>[] is List<S>;
  bool isSupertypeOf<S>() => <S>[] is List<T>;
}

So this way you can test any variable anywhere.

Solido commented 2 days ago

They are similar but your request would require much more upwork to be delivered so I went with just a single method as Munificent expressed the dart does not have much request of those features.

Those previous codes works only on simplistic class extensions but got mixed up pretty quickly in real cases. That's why I ended up with my own code to manage the class types :/

Munificent also shared that the infos is present in the VM but it's mainly a question of priorities.

lrhn commented 2 days ago

that works on simpler cases only.

It works for types, but not really for values.

It does work for all types. If you need to check two types for whether one is a subtype of the other (and presumably at least one is a type variable, otherwise the answer is constant), then doing:

bool isSubtype<T1, T2>() => <T1>[] is List<T2>;

will work for any two types. The only thing one can regret is that it requires allocating an object.

You can use a simple class than List, like:

class _SubtypeHelper<T> {}
bool isSubtype<T1, T2>() => _SubtypeHelper<T1>() is _SubtypeHelper<T2>;

If you have two objects, then asking whether the runtime type of one is a subtype of the runtime type of the other is not possible. It's also not necessarily desirable since it breaks abstraction. You can always ask whether an object has a type that you know. You can't ask if it has a type that you don't know about. That allows objects to have secret, private implementation types that cannot affect other code, because they other code cannot ask about them. However, if you can use the runtime type of an object "as a type", then you can break abstraction and see that, fx, isSubtypeOf(1, 2) but not isSubtypeOf(1, -0x8000000000000000). Why, because 1 and 2 are both instances of Smi, but -0x8000000000000000 is an instance of a different integer implementation type. Using objects as types leaks their runtime type. Not quite reflection, but too close for comfort. Using just their runtimeType and comparisons on type objects may allow over to bypass that, because runtimeType can be overridden to lie about the type. (But there are ways around that, do it can still leak.)

The referenced Java and Kotlin operatons work on Class objects, which would correspond to the Dart Type objects. That again suggests reflection. A corresponding Dart operation could be:

class Type {
  external bool operator <=(Type other);
  external bool operator >=(Type other);
}

which would allow something like T <= Object or T >= Null. (Or similar extension methods, if we don't want to break subtypes of Type.)

That does mean that a type object needs to retain enough information to do subtype checking, which is something AoT compilers can otherwise tree-shake for any type that isn't statically detectable as occurring in a subtype check. Any use of type1 <= type2 can cause the entire type hierarchy of every type mentioned and the runtime type of every object created, to be retained in the compiled program. Today a Type object doesn't retain any type relations.

It's probably clear that I'm fairly strongly against adding reflection-like operations. Java can get away with it, and having full reflection, because it's JIT compiled. It has the source class files available, so anything it hasn't compiled into the JIT'ed program already is still there. Dart is fairly unique in being an interface-based AoT-compiled language. It disallows reflection when AoT-compiled because there is no good way to make it work wellwithout including the entire source program, which is a no-go for AoT compilation, especially for the web. (I'll also admit that I haven't grokked the use case here. There are two implementations of the same API, and someone has to figure out which version a value is from, in order to choose a matching value for it? Could each object just have an API-key that they expose, which says where they come from?)

I'd sugges using dart:mirrors, but mainly because I know it won't work anywhere in practice. :smiling_imp:

Solido commented 2 days ago

Thank you for the very instructive information.

I'm working on a large project with deep and wide implementations and used an Enum inside parent classes for implementation. It's verbose but work as intended as it's a simple field comparaison. It has large unit testing covering.

When replacing this implementation with the specified method they fails with case everything going positive... Should this method able to handle mixins too and rich compositions?

I'm gonna try to find the culprit but for sure they're not behaving the same, not what I would expecting from isSubTypeOf. Enum implementation does like a field inside the Vm would simply, I assume, keep a marker of the parent class.

The use keep is very standard OOP. Replace a default set of implementations with another compatible based on values. But in a collection of subclasses I need to compare with which one is the parent for a transparent swap.

You can't ask if it has a type that you don't know about. You gave a lot of informations and it hints me with a sense of how the Dart VM handle infos, or forget them, but I'll need to spend more time to have a clear vision of what's possible.

Using objects as types leaks their runtime type. Not quite reflection, but too close for comfort. I get that and why I've too deal so often with frustrations in my code because of that but the benefits and security overweight that.

Yet with my respect to the dart team priorities I still feel I'm gonna deeeeply miss this feature ;)

Thanks everyone for helping me out.

dart-github-bot commented 2 days ago

Summary: User requests isSubclassOf function for easier runtime type checking in Dart, similar to Kotlin and Java, to simplify inheritance-based code. Current workaround is insufficient.