Dart 2 Breaking Change: Callable Classes

334 views
Skip to first unread message

Leaf Petersen

unread,
Feb 21, 2018, 5:14:08 PM2/21/18
to Dart Misc
Hi Dart folks - 

We (the Dart team) are preparing to land a breaking change for Dart 2 related to callable classes.  Below is a summary of the changes and how they might impact you.  Feel free to follow up here or with me directly with questions.

What is changing?

In Dart 1, a class with a call method is a subtype of a function type with the same signature.

```dart
// Dart 1
class C {
  void call(int x) {}
}

typedef void F(int i);

void main() {
  C c = new C<int>();
  F f = c; // OK
  f as C; // OK, cast succeeds
  c(3); // OK
  print(f is C); // OK, prints true.

  F f2 = c;
  print(identical(f, c)); // OK, prints true
  print(f == c); // OK, prints true
  print(identical(f, f2)); // OK, prints true
  print(f == f2); // OK, prints true
}
```

In Dart 2, we will continue to allow objects with call methods to be called, and to be assigned to variables of function type.  However, the assignment of an object with a call method to a function type will now be treated as an implicit closurization of the `.call` method of the object.  This changes the behavior of the code snippet above as follows:

```dart
// Dart 2
class C {
  void call(int x) {}
}

typedef void F(int i);

void main() {
  C c = new C<int>();
  F f = c; // OK, treated as F f = c.call;
  f as C; // Cast fails
  c(3); // OK
  print(f is C); // OK, prints false.

  F f2 = c;
  print(identical(f, c)); // OK, prints false
  print(f == c); // OK, prints false
  print(identical(f, f2)); // OK, prints true or false
  print(f == f2); // OK, prints true
}
```

What will break?

Most code that uses call methods is unaffected by this, and will silently continue to work.  

Code that passes off callable objects as functions and then casts them back to the original class type will no longer work.  This is rare, but code such as this can be refactored either to use the Expando class to attach the required state to an actual function, or to explicitly manage state using Maps from the function to associated state.

Code that relies on a callable object being identical to the result of implicitly casting the callable object to a function type will no longer work.  This is rare, but code that relies on this will need to be refactored. 

How do I know if this has broken me?

The most likely error that you will likely see as a result of this change is a runtime cast failure, with a message that looks like this:

Type '(int) => int' is not a subtype of type 'CallableClass'


This error is indicating that a function typed value is being explicitly or implicitly cast to `CallableClass`, where `CallableClass` is some class with a `call` method.  Previously, the value being cast would have been an instance of `CallableClass`, and so the cast would have succeeded.  After this change, the value being cast is the closurization of the `.call` method from `CallableClass`, and cannot be cast back to the original object.  

Why is this change being made?

Callable classes cause complexity in the type system (e.g. https://github.com/dart-lang/sdk/issues/29791), and also cause substantial implementation complexity.  Making uses of callable classes as function types behave as a closurization of the call method supports the vast majority of use cases that we have found, at a tiny fraction of the implementation complexity. 

I will update here when this change lands.  Please reach out to me here or offline with any concerns, and/or with help resolving any issues after this change has landed.

thanks,
-leaf

Anatoly Pulyaevskiy

unread,
Feb 21, 2018, 5:40:15 PM2/21/18
to mi...@dartlang.org
Related question: is implementing Function still considered deprecated in Dart 2? Will it produce some sort of warning similar to those we get when attempting to implement other built-in types (class Foo implements String, for instance)?

--
For other discussions, see https://groups.google.com/a/dartlang.org/
 
For HOWTO questions, visit http://stackoverflow.com/tags/dart
 
To file a bug report or feature request, go to http://www.dartbug.com/new
---
You received this message because you are subscribed to the Google Groups "Dart Misc" group.
To unsubscribe from this group and stop receiving emails from it, send an email to misc+uns...@dartlang.org.

Erik Ernst

unread,
Feb 22, 2018, 5:11:14 AM2/22/18
to Dart Misc
On Wed, Feb 21, 2018 at 11:39 PM, Anatoly Pulyaevskiy <anatoly.p...@gmail.com> wrote:
Related question: is implementing Function still considered deprecated in Dart 2? Will it produce some sort of warning similar to those we get when attempting to implement other built-in types (class Foo implements String, for instance)?

Implementing `Function` has very little value: The static analysis of an instance of type `Function` is done as if that instance had a method named `call` for all possible method signatures (so we basically "turn the type checker off" for such invocations), but the invocation will fail unless the given instance actually has a method named `call` and it is capable of accepting the given actual arguments. So it allows certain unsafe actions to take place.

Conversely, if a class implements a `call` method and does not implement `Function`, an instance of that type will still get the implicit tear-off when used in a context where a function type is required, so `implements Function` will not help "making such instances work like a function".

Dynamically, function invocation will invoke a regular function object (such as the result of evaluating a function literal like `() => 42`), and otherwise it will check whether a non-function-object has a method named `call` and, if so, call that, so `implements Function` again does not help.

All in all, `implements Function` does not provide any other services than allowing your code to fail in some extra ways. ;-)

So, while `implements Function` is not technically prevented (it is not an error), it should not be used.



--
Erik Ernst  -  Google Danmark ApS
Skt Petri Passage 5, 2 sal, 1165 København K, Denmark
CVR no. 28866984

Anatoly Pulyaevskiy

unread,
Feb 22, 2018, 8:41:30 PM2/22/18
to mi...@dartlang.org
Thanks for a thorough reply Erik! 

Your points make sense to me.

Though I feel like if overall recommendation is "it should not be used" then maybe we can give a hint to developers about that? A lint rule or analyzer warning (preferably)?

I have been using it up until recently, until I found a small announcement that it's been deprecated (which is a bit misleading it looks like).

Obviously it is not a big issue but it seems like it can prevent confusion and promote better dev practices.

Rob Becker

unread,
Feb 23, 2018, 6:53:13 PM2/23/18
to Dart Misc
Hi Leaf,
  Would it be possible to make sure that this new proposal for changes to callable classes doesn't break https://github.com/Workiva/over_react ? Or is there a branch or some way for me to test that this doesn't significantly break code at Workiva?

Erik Ernst

unread,
Feb 26, 2018, 4:39:22 AM2/26/18
to Dart Misc
On Fri, Feb 23, 2018 at 2:41 AM, Anatoly Pulyaevskiy <anatoly.p...@gmail.com> wrote:
Thanks for a thorough reply Erik! 

Your points make sense to me.

Though I feel like if overall recommendation is "it should not be used" then maybe we can give a hint to developers about that? A lint rule or analyzer warning (preferably)?

I have been using it up until recently, until I found a small announcement that it's been deprecated (which is a bit misleading it looks like).

Obviously it is not a big issue but it seems like it can prevent confusion and promote better dev practices.

You're right, it is a potential source of confusion, and back in the days where it did not matter it was even considered good style to "document" that the instances of a given class were intended to emulate functions by adding `implements Function`.

So it's also a matter of breakage: How much code will developers have to change, just so they don't do `implements Function` on a callable class?

One consideration that may be relevant here is that compilation to JavaScript can be allowed to let a Dart function compile to a JavaScript function if the Dart function isn't required to be a full-fledged object (so the JavaScript function is only the potential value of a variable of type `Object`, `dynamic`, `void`, and `Function`, and all other variables can be treated in a simpler/faster way because they will be regular objects). In that setting, it would be nice if `f is Function` were a reliable test for being a proper function object, which means that it wouldn't include instances of "callable classes", and that would again fit well with the Dart 2 updates to the notion of callable classes.

But that change would definitely break all those `implements Function` clauses, so we want to discourage that, whether or not there will be some diagnostic message about it. This discussion might help keep that amount of breakage down a bit. ;-)

Leaf Petersen

unread,
Feb 26, 2018, 3:37:45 PM2/26/18
to General Dart Discussion
Rob - What's the best way for us test this?  Would us pulling down that repo and running tests against our prototype give enough coverage?  Otherwise we can look at getting you a branch to test.

-leaf

--

Rob Becker

unread,
Feb 26, 2018, 4:27:43 PM2/26/18
to Dart Misc
Leaf, after discussing it I think the best approach would be to get a branch or build of the proposal that we could use to test agains some internal non-public code to assess the potential impact. How would you like to continue?
Reply all
Reply to author
Forward
0 new messages