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

support covariant parameter overrides #25578

Closed
vsmenon opened this issue Jan 26, 2016 · 37 comments
Closed

support covariant parameter overrides #25578

vsmenon opened this issue Jan 26, 2016 · 37 comments
Assignees
Labels
area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. customer-flutter P2 A bug or feature request we're likely to work on

Comments

@vsmenon
Copy link
Member

vsmenon commented Jan 26, 2016

We currently give a strong mode error on the override in B below:

class A {
  void foo(A a) { print('A $a'); }
}

class B extends A {
  void foo(B b) { print('B $b'); }
}

We could downgrade this to a warning (and in DDC add a corresponding runtime check in B.foo).

@yjbanov @leafpetersen

@vsmenon vsmenon added area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. analyzer-strong-mode labels Jan 26, 2016
@vsmenon vsmenon self-assigned this Jan 26, 2016
@jmesserly jmesserly added the P1 A high priority bug; for example, a single project is unusable or has many test failures label Jan 26, 2016
@jmesserly
Copy link

related: #24507

There was also a request for @covariant or something along those lines, although I can't find the bug for it.

@jmesserly
Copy link

@vsmenon assuming you haven't looked at this? in which case I can do it. Started poking around, looks pretty easy, although I need to add some tracking so we can mark the parameter as needing a cast.

@jmesserly jmesserly assigned jmesserly and unassigned vsmenon Feb 18, 2016
@jmesserly jmesserly added Priority-Medium and removed P1 A high priority bug; for example, a single project is unusable or has many test failures labels Feb 18, 2016
@jmesserly jmesserly removed their assignment Feb 18, 2016
@munificent munificent mentioned this issue Feb 18, 2016
35 tasks
@kevmoo kevmoo added P2 A bug or feature request we're likely to work on and removed Priority-Medium labels Mar 1, 2016
@jmesserly
Copy link

Also related #25232

@jmesserly
Copy link

We may want to merge the various "covariant override support" bugs.

Our most recent thoughts (from a chat with @leafpetersen and @munificent) are to implement an opt-in way to get full support for covariant overrides, rather than downgrade to warning. For example, perhaps something like:

class A {
  void foo(A a) { print('A $a'); }
}

class B extends A {
  void foo(b as B) { print('B $b'); }
}

(syntax is still heavily under discussion, though)

@jmesserly jmesserly changed the title downgrade covariant parameter overrides from error to warning support covariant parameter overrides Jul 19, 2016
@jmesserly
Copy link

if @leafpetersen and/or @munificent find a syntax they like, I can take a stab at implementing this in Analyzer/DDC side.

@jmesserly jmesserly self-assigned this Jul 29, 2016
@jmesserly
Copy link

So one thought that @munificent had -- maybe we should just allow this by default? And Analyzer Strong Mode can mark the AST appropriately for back ends like DDC to know where the casts are.

If that's too permissive, we could add a flag to disable it, or perhaps a warning and then that warning can be suppressed by the usual techniques (per-use, or per-project).

The benefit of this is we don't need new syntax -- we can just allow it. Thoughts @leafpetersen ?

@floitschG
Copy link
Contributor

I vote for just allowing it.

I don't think this was ever a problem so far (which would make me vote for explicit syntax). The cast is local and kind-of expected (no surprise). (That said: we could definitely optimize this case to avoid the check if we know the target).

@eernstg
Copy link
Member

eernstg commented Aug 1, 2016

I think assignability, covariant argument types, and covariance of class type arguments go together: They all introduce implicit downcasts. It's no problem to detect and reject or warn about any of these features, the real issue is how this affects the flexibility that programmers enjoy in return for the unsoundness.

Assignability is a local issue, and programmers could just add the required downcast in case assignability is strengthened to a subtype requirement. But the other two are non-local, so removing the ability to use covariance for argument types and class type arguments will remove the potential for checked-or-strong-mode downcast failures from a set of locations in the code which is much more complex (global analysis is needed in order to detect a superset of the locations where these failures may occur, and it's undecidable whether the "dangerous" types of objects will ever occur there).

Furthermore, covariant argument types and covariance of class type arguments fit in with certain programming idioms where the underlying regularity ensures soundness in ways that the Dart type system cannot capture (e.g., family polymorphism, where classes work together in the same family but objects from different families are never used together). This means that these kinds of covariance are not just about being lazy and taking unnecessary risks, they are just as much about expressing regularities which actually exist, even though they are not enforced fully by the type checker.

From this point of view, I think restrictions on covariance for argument types and class type arguments should be considered together, which also amounts to an argument in favor of allowing covariance for argument types. This means that we keep having defaults which are flexible and convenient, a few steps beyond that which soundness will justify.

That said, it would be nice to have the ability to strengthen checking to the sound level, e.g., by being able to declare that an entire type hierarchy must use sound overriding (no covariant parameter types, no contravariant return types, etc), such that we can have useful knowledge about soundness that developers can use in their reasoning, and compilers may use to produce faster code.

@jmesserly
Copy link

Thanks so much for the thoughts @floitschG @eernstg! I'm definitely persuaded by that. Excellent point too about the symmetry between covariant argument types and covariance of class type arguments. I'll go ahead and implement the "allow it" plan 👍

@leafpetersen
Copy link
Member

I'm quite strongly opposed to implementing this as the default. It is certainly the case that there are valid use cases for covariant overrides. But they are there to support a very specific pattern of programming which is used in specific limited cases, and should always be used with specific intention. Making them the default turns off useful static checking and refactoring tools for all users, for the benefit of the small subset of programs which use this pattern.

If a programmer changes a method signature in a way that is incompatible with overriding methods, they should get feedback from the static checker that they have broken other code. This is one of the key benefits that users get from static checking: it helps make incremental maintenance of large code bases tractable.

Note that empirically, of the large code bases that I've looked at (flutter, internal apps) covariant overrides are by far a corner case.

@yjbanov
Copy link

yjbanov commented Aug 2, 2016

I'm currently leaning towards agreeing with @leafpetersen. If you want to downcast, you can always do it in the method's body (or am I missing something?). That will keep the type hierarchy sound and make it clear to a reader that there is a downcast happening. All of the covariant overrides that I can think of from the existing code bases actually indicated design issues.

Also, it seems the conversion to strong mode can be automated because they are detectable:

BEFORE:

@override
void foo(SubType v) {
  ...
}

AFTER:

@override
void foo(ParentType originalV) {
  SubType v = originalV as SubType;
  ...
}

@leafpetersen
Copy link
Member

To be clear, I'm completely on board with providing opt in syntax to make the compiler generate the boilerplate downcasts. I just don't think it should be the default.

@munificent
Copy link
Member

If you want to downcast, you can always do it in the method's body (or am I missing something?).

Yes, you can, but:

  1. There might be lots of subclasses where you have to do this. The canonical examples where covariant overrides are useful are UI frameworks where you have a base Widget class and tons of subclasses of it that override and specialize some methods. Doing the cast in the body can be annoying boilerplate here.
  2. Then it's not visible in the signature of the method. It's useful in things like API docs to see that an overridden method requires a more specific type.

I agree with Leaf that I'd like this to be explicitly opt-in with some kind of syntax. There's an interesting question about whether the syntax should be in the superclass ("subclasses can tighten this") or in each subclass ("I am tightening this"). My hunch is the former hits the right usability goals.

But getting new syntax in seems to take quite a while. So I wonder if in the short term we should allow covariant overrides with a runtime check and then work towards a notation to make them explicitly opt-in?

@leafpetersen
Copy link
Member

Allowing it by default now and then later on opt-in is just asking for pain. When we make it opt-in, everything has to be fixed up. Code that is currently strong mode clean will drift, and code that is newly converted won't have been fixed up ever, and so we will have a huge mass of code to fix, and we will be sad... :(

@eernstg
Copy link
Member

eernstg commented Aug 3, 2016

Leaf, I can understand your underlying preference for sound rather than
unsound rules, but I don't understand why outlawing covariant argument
types would make a useful difference. As long as we have covariance of
class type arguments we will have exactly the same mechanism from there: If
xs is statically a List<num> but dynamically a List<int>, its add
method will effectively have had an override with a covariant argument type
(num became int). This is the reason why I think those two properties
should be considered together.

For developers, the useful choice is: do I or do I not want to take the
risk that some implicit downcast fails at runtime?

I think a promising approach for allowing developers to make this kind of
choice in a manageable way is to let them specify per subtype hierarchy
that stronger rules must be followed.

The crucial point is that expressions admit subsumption (it's statically
known as a C but at runtime it's a proper subtype D), so properties in
general are only useful if they are guaranteed to hold for all subtypes of
some statically known type. Luckily that's easy to check, for any given
class D the compiler just needs to gather all constraints requested for
the supertypes such as C, and take them into account during compilation
of D.

For instance, we could have modifiers or annotations specifying that a
given class and all its subtypes cannot use covariance in parameter type
overrides or contravariance (including going to dynamic) in return types,
and we could have modifiers or annotations specifying that a given type
argument is invariant rather than covariant, etc.

This would provide added safety in useful chunks, it's not hard to
implement, and it allows for optimizations. E.g., there is no need to
generate type checks on actual arguments in a method if it is always called
safely (which of course means that dynamic calls must go through a stub,
but that should be doable).

Obviously, the default could be the other way around (start out strictly
and allow programmers to request more flexibility), but that's only an
option if the strongest possible set of constraints should apply almost
everywhere. For instance, class type arguments should be invariant except
where explicitly relaxed---which would certainly be a significant change
for the Dartisans.

I tend to think that starting out on the flexible side and building
stronger and stronger guarantees as the software matures is a natural
approach. But this doesn't align very will with an approach where
everything must be heavily annotated at first, and some annotations can
later be removed.

That's the reason why I prefer defaults where strictness is requested
explicitly, but also mechanisms where constraints can be applied to large
and useful portions of code per annotation.

On Wed, Aug 3, 2016 at 3:34 AM, Yegor [email protected] wrote:

Are generics insufficient for expressing "subclasses can tighten this"?

abstract class Renderer { void render(T w); }abstract class ListRenderer extends Renderer { void render(T w); }class InfiniteListRenderer extends ListRenderer { void render(InfiniteList w); }

If generics produce too much boilerplate, perhaps raw types could hoist
the types from method signatures into the type parameters for you. For
example, given the ListRenderer class above:

// raw type extends a generic type// | |// V Vclass InfiniteListRenderer extends ListRenderer {
void render(InfiniteList w);
// ^
// this type is hoisted up into a type parameter
}
// End resultclass InfiniteListRenderer extends ListRenderer {
void render(T w);
}

This way the fix is in the parent class only and it does not require new
syntax.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#25578 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AJKXUvx9XX_tTvZGyk-b0HZfn67vIP4Pks5qb_AWgaJpZM4HMJ4V
.

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

@munificent
Copy link
Member

Are generics insufficient for expressing "subclasses can tighten this"?

Yes, in many cases you can solve it in a sound way using generics. The downside is that it in some cases, it adds a lot of complexity to the entire API. For example, if you change:

abstract class Renderer { void render(Widget w); }

To:

abstract class Renderer<T extends Widget> { void render(T w); }

Then:

  1. Every subclass of Renderer needs to inherit using something like extends Renderer<MyClass> instead of just extends Renderer. It's minor, but defining generic classes is a little spooky to some users that aren't comfortable with them.

  2. Every place you refer to a Renderer needs to provide that type argument. In some cases it's easy if you're only using a Renderer of some specific type. And in those cases, it's great because now you really do get the precise checking you want. But often you have code that deals with renderers in general. If you have some class like:

    /// Renders a thing, uh, later.
    class DeferredRenderer {
      final Renderer _renderer;
      ...
    }

    You need to change it to:

    /// Renders a thing, uh, later.
    class DeferredRenderer<T extends Widget> {
      final Renderer<T> _renderer;
      ...
    }

    So the generic-ness can be viral and end up all over your API. In cases where you have a lot of related classes that all need this pattern (so-called "family polymorphism") you can end up with a slew of classes that all have a long list of type parameters for all of the other classes:

    class Widget<R extends Renderer, S extends State, W extends W> { ... }
    class Renderer<R extends Renderer, S extends State, W extends W> { ... }
    class State<R extends Renderer, S extends State, W extends W> { ... }

    It works, and it's type safe, which is groovy, but it starts to be really cumbersome and feel weird in a language that doesn't encourage a deeply "typeful" user mentality. (In other languages, it's less weird. The above is, I think, pretty typical code in Haskell.)

  3. Places that want to deal with heterogeneous collections of the object are trickier. There is no longer a single "Renderer" class, there's a potentially infinite collection of Renderer classes for different types. But that means that code that wants to work with a bunch of different kinds of renderers gets harder. You can't write:

    /// Renders a bunch of things.
    class BatchRenderer {
      final List<Renderer> _renderers;
    }

    You can't even write:

    class BatchRenderer<T extends Widget> {
      final List<Renderer<T>> _renderers;
    }

    Because your intent is to allow batches to contain different kinds of renderers. Of course, there is no statically sound way to do that, so some amount of casting or runtime checking is implied, but even so, you still need a way to implement it. You could do:

    class BatchRenderer {
      final List<Renderer<Widget>> _renderers;
    }

    And then rely on covariant generics to dodge the issue, but that's kind of sketchy. If we're trying to avoid unsound covariant overrides, shouldn't we avoid unsound covariant generics too? Instead, you end up needing to do something like introducing a separate non-generic base class:

    abstract class WidgetRenderer {
      void renderWidget(Widget w);
    }
    
    abstract class Renderer<T extends Widget> extends WidgetRenderer {
      void renderWidget(Widget w) {
        render(w as T); // Here's the needed runtime check.
      }
    
      void render(T w);
    }

    Then you can do:

    class BatchRenderer {
      final List<WidgetRenderer> _renderers;
    }

    This does work, but, again, it can add a lot of complexity to the API.

So, for Flutter, what I'm planning to do is see where they get covariant override errors and try to make the surrounding code generic to see how much complexity it adds. If it's not too bad, I think it's a viable solution. But in some cases, I think it can end up being not worth the effort.

@munificent
Copy link
Member

As long as we have covariance of class type arguments we will have exactly the same mechanism from there: If xs is statically a List<num> but dynamically a List<int>, its add
method will effectively have had an override with a covariant argument type (num became int). This is the reason why I think those two properties should be considered together.

I totally agree, though for me that's an argument to look into making generic variance type-safe too. :)

I think a promising approach for allowing developers to make this kind of choice in a manageable way is to let them specify per subtype hierarchy that stronger rules must be followed.

I like this, though I think the granularity should be at the member or possibly even parameter level, not the entire class. Covariant overrides are pretty rare, so even in a class that needs them, it probably only uses it on one or two members. The other members in the class shouldn't pay the cost in terms of runtime checking or lack of safety just to please the minority.

Obviously, the default could be the other way around (start out strictly and allow programmers to request more flexibility), but that's only an option if the strongest possible set of constraints should apply almost everywhere.

I look at choosing defaults as basically akin to Huffman encoding. You want to give the most common code the shortest representation. In this case, my experience is that intentional covariant overrides are quite rare. Probably less than 5% of members, maybe closer to 1%. (We can and should scrape a corpus and get real numbers here.) Given that, I think defaulting to not allowing covariant overrides is the right default.

@leafpetersen
Copy link
Member

Leaf, I can understand your underlying preference for sound rather than
unsound rules, but I don't understand why outlawing covariant argument
types would make a useful difference. As long as we have covariance of
class type arguments we will have exactly the same mechanism from there: If
xs is statically a List<num> but dynamically a List<int>, its add
method will effectively have had an override with a covariant argument type
(num became int). This is the reason why I think those two properties
should be considered together.

I don't buy an argument that says that we should do two things wrong instead of just one, because consistency... :)

For developers, the useful choice is: do I or do I not want to take the
risk that some implicit downcast fails at runtime?

This is the key disagreement here: my argument is that this is not the relevant risk. The relevant risk is that some future modification of the code will silently break other pieces of code.

I tend to think that starting out on the flexible side and building
stronger and stronger guarantees as the software matures is a natural
approach. But this doesn't align very will with an approach where
everything must be heavily annotated at first, and some annotations can
later be removed.

That's the reason why I prefer defaults where strictness is requested
explicitly, but also mechanisms where constraints can be applied to large
and useful portions of code per annotation.

I think you are hugely overestimating the frequency of which programmers actually use covariant overrides. There are 184 uses of co-variant overrides in the flutter repo. I don't have an exact count of the number of actual overrides, but a rough textual count gives 2701.

find examples/ packages/ -name "*.dart" | xargs grep "@override" | wc -l

That's less than 10% of overrides, for one of the biggest advocates of this feature. Why are we optimizing for the rare case?

More generally, I advocate for building systems which have "pits of success" to fall into. Most programmers who override a method won't change the signature. They are implementing an interface, and they expect to follow that interface, and they have no interest in thinking about co-variant or contra-variant overrides. The "pit of success" is that the static checking just works for them - they get errors if they use the wrong type, or if they change a superclass type and don't change the subclass type. Their code works, and is maintainable, and they are happy.

@yjbanov
Copy link

yjbanov commented Aug 4, 2016

@munificent I think you might have misunderstood the second part of my comment: "raw types could hoist the types", which seems to address your problems as follows:

Problem # 1: No, you do not need to type MyRenderer extends Renderer<MyClass>, as the <MyClass> type parameter is hoisted up from the overriding method signature. Of course, if MyRenderer itself is abstract and the author didn't care to permit tightening of the type parameter, then further sub-classes will need to cast, but I think by this point we're in a rare use-case territory. User will feel the pain primarily from the most used APIs, such as dart: packages, package:flutter, package:angular2, not from their own classes. Note also, that adding generic parameters to a previously non-generic class is a non-breaking change, which allows starting without generics and adding them later.

Problem # 2: Similarly here, raw Renderer == Renderer<Widget>, and so if DeferredRenderer is concrete (which it is in your example) and works with all Widget types then you do not need to type any generics at all. If DeferredRenderer is designed for sub-classing, then yes you do need to type generics, but only if you think that casting will cause a significant amount of pain. Here too, I think we reduced the number of use-cases drastically.

Problem # 3: I do not fully understand the issue here. I have a feeling that in order for BatchRenderer to even work you need to be able to pick the renderers that apply to the widget you want to render. This is probably better addressed by generic methods:

class BatchRenderer {
  // same as final List<Renderer<Widget>> _renderers;
  final List<Renderer> _renderers;

  void render<T extends Widget>(T widget) {
    _renderers
      .where((r) => r is Renderer<T>)
      .forEach((r) => r.render(widget));
  }
}

@eernstg
Copy link
Member

eernstg commented Aug 4, 2016

On Wed, Aug 3, 2016 at 7:35 PM, Bob Nystrom [email protected]
wrote:

[..] change:

abstract class Renderer { void render(Widget w); }

To:

abstract class Renderer { void render(T w); }

Then:

  1. [..]

  2. [..]
    So the generic-ness can be viral and end up all over your API. [..]
    3.

    Places that want to deal with heterogeneous collections of the object
    are trickier. There is no longer a single "Renderer" class, there's a
    potentially infinite collection of Renderer classes for different types.
    But that means that code that wants to work with a bunch of different kinds
    of renderers gets harder. You can't write:

    /// Renders a bunch of things.class BatchRenderer {
    final List _renderers;
    }

    You can't even write:

    class BatchRenderer {
    final List<Renderer> _renderers;
    }

    Because your intent is to allow batches to contain different kinds
    of renderers. Of course, there is no statically sound way to do that, so
    some amount of casting or runtime checking is implied, but even so, you
    still need a way to implement it. You could do:

An aside: It is actually possible to handle a List<Renderer<T>> where T
is covariant type-safely. One way to do it is to use virtual types which in
this context basically means that you can look up the type argument T from
any given Renderer<T> and you can use that as a type annotation etc. It's
sufficient to just consider a single Renderer because covariance kicks in
already per object, and the List just repeats the same effect.

void some_method(final Renderer renderer)
{
renderer.T myT = renderer.returnSomeT(42);
renderer.acceptListT(<renderer.T>[myT]);
if (myT is int) doAnIntThing(myT);
}

The type renderer.T is an existential type; it is known to be a subtype
of its upper bound U, but no lower bound is known, and this allows us to
safely conclude a few things like renderer.T <: U and renderer.T <: renderer.T, which means that we can safely work on objects with that type
in context of the given renderer. Beyond this, of course, we'll have to
check (as in myT is ..).

Btw, it's unsafe to allow renderer to mutate, hence final is required
if renderer.T is to be used as a type.

Dart isn't likely to get support for virtual types in this century, but
it's still worth noting this part of the language design space.

class BatchRenderer {
final List<Renderer> _renderers;
}

And then rely on covariant generics to dodge the issue, but that's
kind of sketchy. If we're trying to avoid unsound covariant overrides,
shouldn't we avoid unsound covariant generics too?

This was exactly the point I was making: Class type argument covariance
and covariant parameter override are closely related mechanisms, which is
the reason why we should keep an eye on one when talking about the other.

@eernstg
Copy link
Member

eernstg commented Aug 4, 2016

On Thu, Aug 4, 2016 at 3:10 AM, Leaf Petersen [email protected]
wrote:

Leaf, I can understand your underlying preference for sound rather than
unsound rules, but I don't understand why outlawing covariant argument
types would make a useful difference. As long as we have covariance of
class type arguments we will have exactly the same mechanism from there: If
xs is statically a List but dynamically a List, its add
method will effectively have had an override with a covariant argument type
(num became int). This is the reason why I think those two properties
should be considered together.

I don't buy an argument that says that we should do two things wrong
instead of just one, because consistency... :)

Sound type checking vs. type checking that allows some additional programs
isn't the same thing as correct vs. wrong, and I don't actually think it's
easier to design a good unsound type system than it is to design a good
sound one. The former allows more programs, and the question is whether
they are useful programs (rather than just wrong ones). So this isn't a
simple matter of avoiding mistakes.

Covariant argument types and covariance of class type arguments (when used
in contravariant positions) create the same issue at call sites:
potentially failing implicit downcasts which cannot be detected without
global analysis. So my point is that programmers probably want to deal with
the basic issue (do we tolerate that risk?) as such, and then they need
ways to constrain their software in useful chunks (e.g., removing that
danger for all call sites for a specific method or a specific receiver
type, etc). That's the reason why I think it is gratuitously limiting to
just outlaw parameter covariance on its own.

(For a longer discussion about why it makes sense to tolerate potential
downcast failures but outlaw message-not-understood, see
http://www.sciencedirect.com/science/article/pii/S0167642316300831 which is
about the notion of message-safety.)

For developers, the useful choice is: do I or do I not want to take the
risk that some implicit downcast fails at runtime?

This is the key disagreement here: my argument is that this is not the
relevant risk. The relevant risk is that some future modification of the
code will silently break other pieces of code.

That is indeed a relevant danger, but I think we agree that situations like
this should be detected, and developers should be empowered to get
notifications about them if and when they want it.

If we introduce support for quantified constraints (on the form "this
method and its overriders must satisfy property P" or "all methods in this
class and its subtypes must satisfy property P" for certain P) then we can
provide relevant guarantees at a well-known set of call sites. That's what
I'm thinking about when I request consistency.

I tend to think that starting out on the flexible side and building
stronger and stronger guarantees as the software matures is a natural
approach. But this doesn't align very will with an approach where
everything must be heavily annotated at first, and some annotations can
later be removed.

That's the reason why I prefer defaults where strictness is requested
explicitly, but also mechanisms where constraints can be applied to large
and useful portions of code per annotation.

I think you are hugely overestimating the frequency of which programmers
actually use covariant overrides. There are 184 uses of co-variant
overrides in the flutter repo. I don't have an exact count of the number of
actual overrides, but a rough textual count gives 2701.

find examples/ packages/ -name "*.dart" | xargs grep "@OverRide" | wc -l

That's less than 10% of overrides, for one of the biggest advocates of
this feature. Why are we optimizing for the rare case?

Did you check whether 10% or more of all statements are if statements? ;-)

More generally, I advocate for building systems which have "pits of
success" to fall into. Most programmers who override a method won't change
the signature. They are implementing an interface, and they expect to
follow that interface, and they have no interest in thinking about
co-variant or contra-variant overrides.

Nobody stops us from detecting the covariant override and giving a hint
that it might not be intended.

The "pit of success" is that the static checking just works for them -
they get errors if they use the wrong type, or if they change a superclass
type and don't change the subclass type. Their code works, and is
maintainable, and they are happy.

That's great! I just want to give developers the power to manage soundness
related constraints in consistent chunks.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#25578 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AJKXUve09c9K1N723V5iECeAFzGSdyUHks5qcTwTgaJpZM4HMJ4V
.

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

@vsmenon
Copy link
Member Author

vsmenon commented Aug 4, 2016

It looks like we want to support some form of covariance. I think it's worth splitting up the syntax aspect from the soundness aspect.

For syntax, I don't have a whole lot to say beyond a preference for something explicit to indicate the runtime check to the user. I think most of our existing users prefer that and actually see our current type param checks as more a bug than a feature.

For soundness, we already have arguable broken behavior (with generic covariance) where the runtime type of a variable is not a subtype of the static type. I hope we don't make it worse. E.g.,

class Foo<T extends Iterable> {
  T bar(T x) { ... }
}

void main() {
  Foo<Iterable> foo = new Foo<List>();
  var bar = foo.bar;  // Static type is Iterable -> Iterable
  print(bar.runtimeType);  // Prints List -> List
  bar = (bar as dynamic);  // Prints a runtime error in DDC
}

One option is to view covariance as syntactic sugar on a sound pattern. E.g., the above could be lowered to:

class Foo<T extends Iterable> {
  T bar(Iterable _x) { T x = _x as Iterable; ... } 
}

In this case, the runtime type of the tearoff is Iterable (the type bound) -> List.

@munificent
Copy link
Member

@munificent I think you might have misunderstood the second part of my comment: "raw types could hoist the types", which seems to address your problems

Ah, sorry, you're right. I was looking at this in terms of how you'd solve it using generics without any other language changes. Your suggestion is pretty interesting in that it gives you the safety of more generics but alleviates some of the usability problems.

My thirty second intuition is that the implicit hoisting probably has some weird failure modes. Like what happens if you have two overridden methods that provide different parameter types? Do you hoist their least upper bound? Will it cause problems when a class evolves over time if it does so in a way that changes the implicitly hoisted generics?

But I haven't put much thought into it. It is a really neat idea. I'll ponder it more.

@yjbanov
Copy link

yjbanov commented Aug 4, 2016

implicit hoisting probably has some weird failure modes

Yeah, more generally what I'm implying is that there are no raw types any more, not statically nor at runtime. Now if there are no issues with eliminating raw types, then the interpretation of the syntax of raw types is up for grabs. I suggest that we use it for "reasonable defaults" for generic parameters that you do not wish to type. My gut feeling is that it's very doable. As an example, C# doesn't have raw types, and in fact doesn't use raw type syntax for anything (it's illegal). I think that's also the case in Swift.

@munificent
Copy link
Member

As an example, C# doesn't have raw types, and in fact doesn't use raw type syntax for anything (it's illegal).

Actually, C#'s story is a little different. Types can be "overloaded" by type parameter arity. So you can define separate classes, Foo, Foo<T>, Foo<T, S>, etc. So there are no real "raw" types. Or another way to look at it is C# lets you control what the raw type represents in any way you want.

It ends up being really handy. See, for example, the different forms of Tuple, Func, and Action.

@yjbanov
Copy link

yjbanov commented Aug 4, 2016

Types can be "overloaded" by type parameter arity

Yeah, if there's any chance that Dart will support this in the future, then there's more to think about. Perhaps arity can be hoisted too using structural matching if possible?

abstract class Tuple<T1> { T1 get value1; }
abstract class Tuple<T1, T2> { T1 get value1; T2 get value2; }

// structurally only matches Tuple<int, int>
class IntTuple extends Tuple {
  @override int get value1 => 1;
  @override int get value2 => 1;
}

// error: ambiguous - could mean either Tuple<int> or Tuple<int, dynamic>
abstract class FunkyTuple extends Tuple {
  @override int get value1 => 1;
}

The compiler would then have to do structural matching, something akin to interfaces in Golang. Or the whole situation could be considered ambiguous and the developer is forced to type out generics exactly. The issue is that introducing new types with different arities could be a breaking change.

@munificent
Copy link
Member

Yeah, that level of structural typing feels out of place to me for a language like Dart.

@jmesserly
Copy link

jmesserly commented Aug 10, 2016

this is not a fix for this issue, but fixes another bug I found while investigating: https://codereview.chromium.org/2236763002/, so I'll probably want to land that first. We also appear to be checking overrides in two places (checker.dart and error_verifier.dart) but more disturbing was they were using different logic (which that CL should hopefully fix).

@jmesserly
Copy link

fyi, I'm blocked for now on landing either this or something like it: https://codereview.chromium.org/2246293002/, basically we need to remove the duplicate override checking

@jmesserly
Copy link

@leafpetersen -- it sounds like we might want this for callbacks as well? based on flutter/flutter#5689 (comment)

    <Type, GestureRecognizerFactory>{
        // fn type is Object -> TapGestureRecognizer
        TapGestureRecognizer: (@covariant TapGestureRecognizer recognizer) {
          // implicit cast recognizer to TapGestureRecognizer
          return (recognizer ??= new TapGestureRecognizer())
            ..onStart = _handleOnStart;
        }
      },

@leafpetersen
Copy link
Member

We may want to prototype this, but I need to chat with @Hixie a bit more first. He's expressed a preference for a mechanism that puts the burden on the API rather than on the client of the API. It's not exactly clear to me yet how that should be surfaced in code like this.

@leafpetersen
Copy link
Member

@sethladd

@leafpetersen
Copy link
Member

Note, after some discussion we chose to go with @checked (available from package:meta) as the prototype syntax, since it was felt that this was more descriptive of the intent and might generalize more naturally to other use cases.

@leafpetersen
Copy link
Member

With this in place, in strong mode, you can now mark any given parameter type of a method with the @checked annotation. Unlike normal parameter types which much be supertypes of the parameter that they override, parameters marked with @checked may be super or sub types of the parameter that they override. Note that @checked can be added at any point in the hierarchy, and applies both to the marked method, and to all overrides of the marked method.

class A {
  void f(Object x) {};
}

class B extends A {
  void f(@checked num x) {}; // Allowed, even though num <: Object
}

class C extends B {
  void f(int x) {};  // Allowed again, since a superclass has opted in
}

On DDC, and on other eventual strong mode platforms, there will be an implied runtime type check inserted in the header of the method to verify that the argument passed is of the correct type.

Tearing off a method which has been marked with @checked results in a function whose runtime type is one in which each parameter that is marked as @checked (either explicitly or in a superclass) is treated as having type Object. So for example, given the above code:

typedef void F(Object x);

void main() {
  var c = new C();
  assert(c.f is F);  // Always true.
}

Covariant overrides are checked against all overridden methods. This means that you cannot override with arbitrary types. The following is not allowed:

class A {
  void f(Object x) {};
}

class B extends A {
  void f(@checked num x) {}; // covariant override
}

class C extends B {
  void f(Object x) {}; // contravariant override, ok  
}

class D extends B {
  void f(String x) {}; // ERROR.  A valid covariant override from Object, but not from num
}

The @checked annotation is currently supported on setters, but not yet on fields. Prototype support for the latter is planned: #27363 .

@Hixie @abarth @sethladd

@Hixie
Copy link
Contributor

Hixie commented Sep 17, 2016

Tearing off a method which has been marked with @checked results in a function whose runtime type is one in which each parameter that is marked as @checked (either explicitly or in a superclass) is treated as having type Object.

This seems weird. What are the implications?

Can I do this?:

typedef void F(String s);
class A { void f(@checked num x) { } }
class X { F h; }

void main() {
  A a = new A();
  X x = new X();
  x.h = a.f;
}

@vsmenon
Copy link
Member Author

vsmenon commented Sep 19, 2016

The static type of the tearoff a.f should still be num -> void, but the runtime type would now be Object -> void.

So, that example should still be a static type error on the assignment to x.h.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. customer-flutter P2 A bug or feature request we're likely to work on
Projects
None yet
Development

No branches or pull requests

10 participants