Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove dynamic type #3192

Open
maeddin opened this issue Jul 4, 2023 · 22 comments
Open

Remove dynamic type #3192

maeddin opened this issue Jul 4, 2023 · 22 comments
Labels
request Requests to resolve a particular developer problem

Comments

@maeddin
Copy link

maeddin commented Jul 4, 2023

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:

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 for dynamic 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 of dynamic prevents other features that many developers would like to use in a type-safe object-oriented language. For example, private, protected and public modifiers are not possible because they could not support dynamic 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 by dynamic 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 to Object? 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.

@lrhn
Copy link
Member

lrhn commented Jul 4, 2023

This is a language change request. Moving it to the language repository.

@lrhn lrhn transferred this issue from dart-lang/sdk Jul 4, 2023
@lrhn lrhn added the request Requests to resolve a particular developer problem label Jul 4, 2023
@mateusfccp
Copy link
Contributor

mateusfccp commented Jul 4, 2023

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.

@rrousselGit
Copy link

rrousselGit commented Jul 4, 2023

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

@rubenferreira97
Copy link

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?

@srawlins
Copy link
Member

srawlins commented Jul 4, 2023

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: dart-lang/lints#44 😁

@ykmnkmi
Copy link

ykmnkmi commented Jul 5, 2023

I like it. I have been using strict type checks and explicit casting from the beginning using the Dart language.

@lrhn
Copy link
Member

lrhn commented Jul 5, 2023

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.)

@mateusfccp
Copy link
Contributor

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 as dynamic, 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.

@maeddin
Copy link
Author

maeddin commented Jul 5, 2023

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 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.

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 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.

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 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.

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.

@lrhn
Copy link
Member

lrhn commented Jul 5, 2023

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.

@maeddin
Copy link
Author

maeddin commented Jul 5, 2023

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.

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.

@rrousselGit
Copy link

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.

@goderbauer
Copy link

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.

@incendial
Copy link

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?

@rrousselGit
Copy link

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

@lrhn
Copy link
Member

lrhn commented Jul 5, 2023

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.

@gintominto5329
Copy link

Could be related: dart-lang/sdk#50874

@lucavenir
Copy link

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.

@ds84182
Copy link

ds84182 commented Jul 9, 2023

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.

@maeddin
Copy link
Author

maeddin commented Jul 11, 2023

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.

@lrhn
Copy link
Member

lrhn commented Jul 11, 2023

(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.

@ds84182
Copy link

ds84182 commented Jul 11, 2023

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.

nickmeinhold added a commit to enspyrco/json_utils that referenced this issue Jul 25, 2023
Discussion in dart-lang/language#3192
indicates a general move away from dynamic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests