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

class declaration shorthand syntax doesn't allow implementing simple interfaces #3120

Open
jakemac53 opened this issue May 31, 2023 · 10 comments
Labels
request Requests to resolve a particular developer problem

Comments

@jakemac53
Copy link
Contributor

jakemac53 commented May 31, 2023

Consider the following example:

abstract interface class A {}

class B {}

class C = B implements A;

This gives the following errors for the class C:

  • Expected to find 'with'
  • Classes can only mix in mixins and classes

The interface class A cannot be mixed in, so you are forced to instead use the full class syntax:

class C extends B implements A {}

This isn't the end of the world or anything, but it does also seem like the requirement for a with clause isn't actually necessary, and with class modifiers you can't use the workaround (with). You have to resort to the full class syntax.

@jakemac53 jakemac53 added the request Requests to resolve a particular developer problem label May 31, 2023
@rrousselGit
Copy link

A workaround is to make a temporary mixin

mixin _A implements A {}

class C = B with _A;

@jakemac53
Copy link
Contributor Author

A workaround is to make a temporary mixin

Well, that is more boilerplate than just doing the regular class declaration.

Mostly I just filed this issue because it seems like a bit of an unnecessary restriction, there are certainly workarounds.

@eernstg
Copy link
Member

eernstg commented May 31, 2023

Note that this primary constructor proposal includes the ability to use ; as a class body:

class C extends B implements A;

The semantics is different (because class C here has B as its direct superclass, whereas class C = B implements A; would presumably make C an identical copy of B, except that C is a subtype of A, and except that B and C would presumably be considered nominally distinct types). However, this is not easy to observe unless the setting is some kind of meta-programming, static or dynamic.

@jakemac53
Copy link
Contributor Author

Note that the more annoying part about this is actually that you end up having to duplicate any constructors. In my actual use case there is a single unnamed constructor with 5 parameters that I end up having to duplicate using the standard class pattern with extends.

Using the mixin trick does let me avoid that, but ultimately I own the interface and so I can just make it be a mixin, even though its really just an interface and it makes me cringe to do so.

@rrousselGit
Copy link

Well, that is more boilerplate than just doing the regular class declaration.

It depends. Since the class shorthand also copies constructors

But overall I agree. Although I don't think that syntax is used a lot.

@jakemac53
Copy link
Contributor Author

It depends. Since the class shorthand also copies constructors

Yeah, once I realized my actual use case was relying on the constructor also (probably the only reason I used this syntax to begin with), I switched to the mixin workaround.

@lrhn
Copy link
Member

lrhn commented May 31, 2023

There is no "class declaration shorthand", the syntax is called "mixin application class declaration" which is why the with is mandatory.

It's not the declaration which copies constructors, every mixin application adds forwarding constructors, and that is why

class S {
   S.foo();
}
class C extends S with M {
  C() : super.foo();
}

works - the S with M mixin application class which is the superclass of C gets forwarding constructors that super.foo() can refer to.
It's just that the mixin application class declaration class C = S with M; has no explicit class body on top of the mixin application of S with M, like class C extend S with M { ...explicit body ...} does. The class body has to write its own constructors, the mixin applications get forwarders.

The problem with class C = S implements I; is that every class in Dart extends a superclass with a "mixin" (either an explicit class body or using with), but this class C does not have any mixin to apply. We could make it automatically apply an empty mixin, but as a mixin application rather than an empty body so that it gets forwarding constructors. But it's somewhat arbitrary.

Or we can allow a class to not apply a mixin on top of its superclass, while still adding more interfaces. It's not inconceivable, but it may require changing the internal model of some tools.

I'd prefer to not touch this now, and see if we can go with the primary-constructor ; syntax, if that can be allowed to mean no body instead of empty body - maybe only if you have no primary constructor. (Same issue again, but part of a larger feature, so more reasonable to do a rewrite of internal models.)

@jakemac53
Copy link
Contributor Author

Ah ok, the details of how it works makes sense why with is required. I think this also didn't matter prior to class modifiers, but now it does hold a bit more weight.

But I agree it doesn't need to be highly prioritized or anything. It just feels like an arbitrary restriction as a user of the feature, who thinks of this as just a "class declaration shorthand syntax".

@lrhn
Copy link
Member

lrhn commented May 31, 2023

There is nothing fundamentally preventing us from just allowing class C = S implements I; or even class C = S;. It differs from the current underlying model (which may be more of a mental model than an explicit definition), but we can choose another consistent model where it works. In fact, it might be simpler if we don't require an explicit chunk of declarations on top of a class to create a subclass.

For example, we could just say that class C = S implements I; defines a class C with superclass S, which has an interface which is the combination of the interfaces of S and I (like we normally combine interfaces), and member implementations which are the same as those of S (possibly with a few overriding synthetic member implementations added to account for noSuchMethod forwarders or covariant forwarders due to I), and constructors which mimic/forward to those of S.

Basically, it's the same as class C extends S implements I {} except that you get constructor forwarding instead of having to declare constructors yourself inside the body. (Or get only the default constructor.)

It can easily work.
But why can't you add instance methods and still get constructor forwarding? Maybe we can write that as

class C extends S with {
  int foo() => 42;
}

which then acts like a mixin application and inherits constructors, but cannot write any of its own.
(But not great syntax, and creeping closer to #698, which was totally feature-creeped.)

If we want to make that change, and are therefore making changes at all, I'd prefer to also change the = syntax to just class C extends S implements I;, with precisely the same meaning.

So:

class C extends S;  // forwarding constructors.
class C extends S with M; // forwarding constructors.
class C extends S {} // No forwarding constructors, only default constructor  `C(): super();`
// **Maybe**
class C extends S implements I with { int foo() => 42; } // forwarding constructors again.

Then we can still explain that class C extends S with M1, M2 {} is equivalent to

class S_M1 extends S with M1;
class S_M1_M2 extends S_M1 with M2;
class C extends S_M1_M2 { ... }

where each declaration introduces a class, with a superclass and something added on top.
We can just also write class C extends S; to make a completely trivial subtype of S.

@eernstg
Copy link
Member

eernstg commented Jun 1, 2023

@lrhn wrote:

class C extends S;  // forwarding constructors.

That's a very interesting idea! But we might want to give ourselves as developers more power, say, covering the case where we want to obtain some forwarding constructors, but not all; or we want to get some/all forwarding constructors, but the class body is not empty (because we want to declare all kinds of other things).

Sounds like a job for export ;-), #2506.

class A { A.one(); A.two(); }

class B extends A {
  static export show A.one, A.two; // Ad-hoc syntax, but you get the idea.
  ... // Other declarations.
}

We could then specify a particular default export clause which would serve to establish the implicit constructor forwarding as described in previous comments. Anyone who wants a non-standard behaviour could then simply write an explicit export clause.

class B2 extends A {
  static export show A.*; // Possible default for a class body specified as `;`.
}

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

4 participants