Open maeddin opened 1 year ago
This is a language change request. Moving it to the language repository.
I don't use dynamic
in ages. Everything can be written with Object?
instead, except for maybe some sketchy edge-cases.
I see no downsides except for the fact that this is a very breaking change.
I fully support this change, but I find it hard to believe that the Dart team would go through this route, as they are pretty conservative about breaking changes.
Same here. I've stopped using dynamic forever ago.
The only use-case I can think of today is mocking, which currently often relies on dynamic invocation with noSuchMethod.
There's also how dart:convert does a dynamic invocation of toJson
So removing it could be quite breaking. Although not impossible. For instance, mockito already uses code-generation. So it could stop relying on dynamic altogether
I am also in favor of this change. As far as I know, the dynamic
type introduces many edge cases to the language.
However, a question arises: Would removing the dynamic
type from Dart pose a challenge for implementing interoperability with other languages, such as JavaScript?
A stepping stone to removing dynamic from the language would be to ban dynamic calls in the recommended (or core) lint rule sets. You can upvote here: https://github.com/dart-lang/lints/issues/44 😁
I like it. I have been using strict type checks and explicit casting from the beginning using the Dart language.
Is it the dynamic
type existing, which is the problem, or its accidental use?
What if the only expression that could have type dynamic
was e as dynamic
, effectively making as dynamic
a special syntactic form with special rules for member access and assignability, but dynamic
couldn't otherwise be used as a type?
Then you can still ask for a dynamic invocation if you really need it, but it absolutely certainly cannot happen by accident, or because someone else chose dynamic
for you.
Dynamism would then be an opt-in feature, with a clear syntactic marker everywhere it's used. But you'd still have the power when you really need it.
The alternative is that there are existing features which cannot be retained. Like jsonEncode
trying to call toJson
, which is admittedly no great loss, but still an unmitigatable breaking change.
It might encourage introducing new, simple, interfaces and force existing code to implement those, in order for existing dynamic code to keep working. Not sure having lots of small interfaces is a win. And without interface injection, it requires total cooperation.
(Not saying we cannot remove dynamic
entirely, which I would have said before patterns, but it is a distinguishing feature of Dart that it is there, as a backdoor when what you want to do isn't something you can convince the type system of. Like late
and as
, ways to tell the static analysis that you do in fact know better. Which you better do then. But you're not asking to remove as
, which is just as unsafe as dynamic
, so I'll presume it's more about performance, and then just not using dynamic
should be enough. Avoiding accidental use should put that within your own control.)
What if the only expression that could have type dynamic was e as dynamic, effectively making as dynamic a special syntactic form with special rules for member access and assignability, but dynamic couldn't otherwise be used as a type? Then you can still ask for a dynamic invocation if you really need it, but it absolutely certainly cannot happen by accident, or because someone else chose dynamic for you.
This would be better than what we have today and less breaking, so it may be an alternative path. From there we can think if it's really valuable to remove dynamic
entirely.
But you're not asking to remove
as
, which is just as unsafe asdynamic
, so I'll presume it's more about performance, and then just not using dynamic should be enough.
Honestly, I would also ask to remove as
if we could improve our type system in the areas that today require us to use as
. There are a few cases where I have to use as
because I can't express what I want with the type system, and every single time I do this I become completely afraid of having runtime exceptions because I made something wrong. Yes, I am telling to the compiler that I know better, but the fact is that I am not 100% sure that I know better, it's only that I have no other way to to what I have to do.
Is it the
dynamic
type existing, which is the problem, or its accidental use?
It is even the existence of dynamic
for me, but through it, of course, the accidental use of it also.
What if the only expression that could have type
dynamic
wase as dynamic
, effectively makingas dynamic
a special syntactic form with special rules for member access and assignability, butdynamic
couldn't otherwise be used as a type? Then you can still ask for a dynamic invocation if you really need it, but it absolutely certainly cannot happen by accident, or because someone else chosedynamic
for you.Dynamism would then be an opt-in feature, with a clear syntactic marker everywhere it's used. But you'd still have the power when you really need it.
In my opinion, dynamic
could be left optional in the language, as long as there is no performance loss for the language in general in the compiled code (so also e.g. for dart2wasm) if you don't use dynamic
in it explicitly. Unfortunately, I cannot assess this, since I have little idea of the concrete implementation of the compilers.
If the compromises currently made for dynamic
are too much, it should be removed completely. Because this way you could achieve not only clean code but also better native performance.
The alternative is that there are existing features which cannot be retained. Like
jsonEncode
trying to calltoJson
, which is admittedly no great loss, but still an unmitigatable breaking change.It might encourage introducing new, simple, interfaces and force existing code to implement those, in order for existing dynamic code to keep working. Not sure having lots of small interfaces is a win. And without interface injection, it requires total cooperation.
For jsonEncode
, you could either introduce a simple interface or have jsonEncode
only support Maps, Lists, Strings, Numbers, etc. and you would have to call the toJson()
methods of your own classes yourself before passing the result into jsonEncode
. Either way, this wouldn't be the most drastic change (as you have already stated).
Too many small interfaces would of course be annoying, although you need at most one interface in most cases anyway. Besides, a lot of things (currently) are done by code generation, so for most use cases there would be no need for interfaces at all (unless I'm missing something here).
However, interfaces would be a compromise that can be made for compile time type checking. In the end, using interfaces is still cleaner than calling methods like toJson()
through the dynamic
type.
But you're not asking to remove
as
, which is just as unsafe asdynamic
, so I'll presume it's more about performance, and then just not usingdynamic
should be enough. Avoiding accidental use should put that within your own control.
as
should not be removed, because you actually need casts often. In Dart this happens implicitly most of the time, but the static analysis can't find everything, so sometimes you have to do it manually. And unlike dynamic
, for using as
you need to know the type, on which you want to call a method.
as
should not be removed, because you actually need casts often
You never need a cast, you can do an is
check to promote instead. It's even easier now with patterns.
You then have to handle the else branch yourself, and hopefully throw a better error message than TypeError: A 'Foo' is not a 'Bar'.
That's just cumbersome, in the cases where you do know that the value will definitely have the assumed type. But you're depending on dynamic knowledge that the compiler cannot verify, setting yourself up for runtime failure, with no static warning possible, if you make a mistake.
Implicit downcast from dynamic
is just the same as an as ContextType
. That's not the problematic feature. It's the dynamic invocation which cannot be simulated without knowing all possible types.
as
should not be removed, because you actually need casts oftenYou never need a cast, you can do an
is
check to promote instead. It's even easier now with patterns. You then have to handle the else branch yourself, and hopefully throw a better error message thanTypeError: A 'Foo' is not a 'Bar'.
That's just cumbersome, in the cases where you do know that the value will definitely have the assumed type. But you're depending on dynamic knowledge that the compiler cannot verify, setting yourself up for runtime failure, with no static warning possible, if you make a mistake.
Using is
leads to an implicit cast. This is a somewhat nicer thing to do, but nothing different. I almost never use as
myself because of is
and patterns.
That was not well expressed by me, sorry.
But this issue is primarily about dynamic
and not about as
, which is why this discussion would probably be better suited in another issue.
I think the idea of having dynamic be more explicit is a good one.
Changing type inference to instead rely on Object? instead of dynamic would be great.
I've done that in Freezed by changing fn<T>(...)
to fn<T extends Object?>()
in generated code.
This seems to tell type inference to not rely on dynamic
There are also a bunch of APIs which promote dynamic. Like jsonDecode. It'd be nice if the SDK and Flutter stopped using it. That would push folks to do the same too.
Is it the dynamic type existing, which is the problem, or its accidental use?
For me, it is the accidental use. If you'd have to be very very explicit every time you want to do a dynamic invocation it wouldn't be a problem anymore.
It'd be nice if the SDK and Flutter stopped using it.
Wondering if you have any examples where Flutter is using dynamic. We try very hard not to and I'd be open to removing any remaining instances.
Wondering if you have any examples where Flutter is using dynamic. We try very hard not to and I'd be open to removing any remaining instances.
@goderbauer here is the report of avoid-dynamic
on the whole Flutter repo (beta branch) report.txt. Will this work?
Wondering if you have any examples where Flutter is using dynamic. We try very hard not to and I'd be open to removing any remaining instances.
Honestly I grouped flutter and Dart SDK together. It could be that Flutter doesn't really use it, I don't remember.
The sdk uses it quite often I think. In part with error handling or json
The sdk uses it quite often I think. In part with error handling or json
Mainly JSON, because JSON is inherently untyped, and Dart 1 used dynamic
to mean "this cannot be typed". Also to make it easier to use. Now that the rest of the language is actually typed, removing dynamic
from JSON would just be breaking.
Not that I don't want to, but it had to piggy-back on some larger related change, it's not worth the breakage by itself.
Some async error handling uses Function
, but only because it has to accept two incompatible function types, not necessarily because it wants to call them dynamically. It would still work if you couldn't call something typed as Function
dynamically.
But there are places in the SDK libraries where some raw type gets instantiated with dynamic
, and we can't fix that because someone, somewhere, depends on it. That's just annoying.
Could be related: https://github.com/dart-lang/sdk/issues/50874
it had to piggy-back on some larger related change, it's not worth the breakage by itself.
Well, yes, I guess that when there's a breaking change, one would try to fit in good counter measures as much as possible. I guess that rarely you'd release, say, "Dart 4" just because of a single feature like this one.
If anything, I wish a potential "Dart 4" version would remove dynamic
entirely, would uniform the switch
syntax and thus would offer easier pattern matching syntax. Also a public
/ private
/ protected
solution would be nice. And, of course, metaprogramming. I know, I am being imaginative.
But there are places in the SDK libraries where some raw type gets instantiated with dynamic, and we can't fix that because someone, somewhere, depends on it. That's just annoying.
I can't see how this is insurmountable. By definition, a breaking change un-supports ..stuff! Folks would need to adapt to it. SDK / Flutter / core libraries included.
... But I sincerely can't wait to do so! If there's a fair amount of benefits that indirectly affect or bring other features, I'd quickly transition to this. As many others, I don't use dynamic
, either. And as Remi just said, it'd be nice if the whole ecosystem stopped using it. Like, to deprecate it at some point. That's a good incentive for the whole community.
I don't think dynamic should go away entirely, but it should be made safer. A form of structural typing can be introduced to make dynamic a bit more ergonomic. For example, toJson in dart:convert
json encoding:
// Interface that can be explicitly or implicitly implemented by a type
// as long as it contains all matching members.
typedef interface ToJson {
Object toJson();
}
// ... json innards in dart:convert when handling an unknown type:
// assume object is declared as `dynamic object;`.
if (object is ToJson) {
object = object.toJson();
}
Downstream consumers can choose to use an implements clause for ToJson. But otherwise it can be omitted.
class MySerializableClass implements ToJson {
Map<String, dynamic> jsonEncode() => {"foo": "bar"};
}
Then warn when dynamic is used unexpectedly. Tell the developer to use an explicit type or a structural interface instead.
The difference between dynamic
and Object
is that the former may be structurally casted. The latter should not be. This maintains dynamic semantics in the language without it being a footgun. And it covers a majority of places where dynamic is still used today.
Downstream consumers can choose to use an implements clause for ToJson. But otherwise it can be omitted.
This sounds like a method to prepare for the breaking change, but not to prevent a breaking change. Here, one would continue to support something that is officially not recommended. You could throw out the support for calling toJson()
without the interface with the next major Dart release. In the long-term one should not support the interface and dynamic
calls for json encoding, as it offers no noticeable advantage and can be confusing for users.
(I'd rather just stop calling toJson
entirely, interface or no interface, and require you to pass the toEncodable
function. Then you can make your own ToJsonable
interface and a toEncodable
function which calls it.)
But that's just one example of code doing dynamic calls. We have no idea how many deliberate dynamic calls happen in client code, or even inside the VM's own libraries. I know there are some.
Flutter has various usages of dynamic, notably: https://github.com/flutter/flutter/blob/5d4a1f1f5fe1d0d0191c691eb60b6814654d6929/packages/flutter/lib/src/animation/tween.dart#L339
Which is intended to avoid having to make N different implementations of Tween to support all kinds of numeric-like classes (Offset for example, maybe Vector2/3/4 from vector_math). Could be supplanted by structural interfaces.
Proposal
Dart should remove the
dynamic
type for stricter type-safety, better performance and less support for bad practices.Justification
The use of
dynamic
is a bad practice, often even referred to as such in the documentation:dynamic
: Indicates that you want to disable static checking. Usually you should useObject
orObject?
instead." (https://dart.dev/language/built-in-types)However, there are ways to bypass
dynamic
calls and implicit casts for your own projects using lints (e.g. with strong-mode). But this does not solve the problems that the support fordynamic
has for the Dart language in general. Firstly, it results in worse performance of the compiled code, since more inefficient runtime checks are required. (I'm not an expert on Dart compilers, so correct me if I'm wrong on this point). Secondly, the support ofdynamic
prevents other features that many developers would like to use in a type-safe object-oriented language. For example,private
,protected
andpublic
modifiers are not possible because they could not supportdynamic
types except through very inefficient runtime checks (dart-lang/sdk#33383).In general, Dart tries to be as type-safe as possible. But on the other hand there is this relict
dynamic
, which stands against it.dynamic
generally feels like a concession to JavaScript developers to get more of them to switch to Dart/Flutter. Developers who have moved from Java/Kotlin/Swift/C++,... are just disturbed bydynamic
and it also doesn't bring any benefits except more opportunities to apply bad practices.Impact
All code that currently uses
dynamic
would have to be changed toObject?
or subtypes and implicit casts would be required. Also, method calls would no longer be possible without knowing the type of an object.Mitigation
Since this would be a null-safety level change, it would have to be made optional at first and fully introduced with the next major release.