Skip to content

extend or implements from a generic type #3552

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

Closed
AdamRose66 opened this issue Jan 11, 2024 · 11 comments
Closed

extend or implements from a generic type #3552

AdamRose66 opened this issue Jan 11, 2024 · 11 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@AdamRose66
Copy link

AdamRose66 commented Jan 11, 2024

I'd like to be able to do something like this:

class A<T> extends T { ... }

or

class B<T> implements T { ... }

@AdamRose66 AdamRose66 added the feature Proposed language feature that solves one or more problems label Jan 11, 2024
@Wdestroier
Copy link

How are you going to use the B class after that?

@mateusfccp
Copy link
Contributor

mateusfccp commented Jan 11, 2024

I am not sure what you are trying to achieve.

What would be the use-case of this esoteric approach? What problem are you trying to solve with this?

I don't even know if this is possible, as we would have to solve a lot of problems, like, how would we dynamically implement the interface we are instantiating?

There's probably a better way to solve what you are trying to solve, whatever problem it is...

@lrhn
Copy link
Member

lrhn commented Jan 12, 2024

That's very unlikely to happen.

The problem is that class A<T> extends T

  • Doesn't know its superclass.
  • Therefore cannot invoke any super. members.
  • Doesn't know its superclass's constructors (which its own constructors need to invoke).
  • Doesn't know its superclass's interface.
  • Therefore doesn't know it's own interface.
  • Can't safely declare any members, because they might conflict with the superclass member signatures, which it doesn't know.

Basically, it's impossible to do anything safely or soundly, which means that this particular design is unsuited for the Dart language.

A "class" which doesn't know its concrete superclass, but may have an idea about its interface, is what a mixin is.
It's a class which takes its superclass as an "argument", but it only gets the superclass at mixin application, where it creates a new class with a single fixed interface.

@luizbarboza
Copy link

What I think he's really trying to say is that he'd like to be able to guarantee that type T is a supertype of A.

Something like:

class A<T super A> {}

Related to #1674.

@AdamRose66
Copy link
Author

The actual requirement is more for

class A<T> implements T

than

class A<T> extends T

Or more precisely

class A<IF> implements IF

where A is an abstract interface class.

eg

class Proxy<IF> implements IF
{
   ...
   noSuchMethod( Invocation invocation )
   {
     reflect( port_if ).delegate( invocation );
  }

  IF get port_if => m_if;

  IF m_if;
}

abstract interface class MyInterface
{
  void my_message( int n );
}

Proxy<MyInterface> a = A( ... );

a.my_message( 3 );

The idea is that A is a proxy for an abstract interface that we can use to specify the relationships between interfaces

At the moment I have to do:

class Proxy<IF> // no implements 
{
   ...
   noSuchMethod( Invocation invocation )
   {
     reflect( port_if ).delegate( invocation );
  }

  IF get port_if => m_if;

  IF m_if;
}

abstract interface class MyInterface
{
  void my_message( int n );
}

// tedious typing needed here ...
class MyProxy extends Proxy<MyInterface> implements MyInterface
{
}

MyProxy a = A( ... );

a.my_message( 3 );

@AdamRose66
Copy link
Author

Now, I completely agree that using noSuchMethod in this way means that we are bypassing the static checking that would otherwise be a very good thing.

If there were some checkable way of delegating an implementation to an interface, then that would be better.

@AdamRose66
Copy link
Author

Also, I can hear the chorus of replies "that's what all the meta-programming stuff is for". But waiting for that seems overkill for something that is almost already there.

@AdamRose66
Copy link
Author

And of course we can ( from the original example ) do:

Proxy<MyInterface> a = A( ... );

a.port_if.my_message( 3 );

In which case, you can re-interpret my request as the ability to overload the '.' operator :).

@lrhn
Copy link
Member

lrhn commented Jan 12, 2024

I was expecting something like a proxy, where the type to be proxied is determined dynamically by the type argument.
And for all the reasons given above, that's just not a good fit for Dart.

I'll say that implements T is significantly more likely than extends T, which still only brings it barely above zero. (Allowing extends T is so outrageous that I'd assign it a negative priority — we'll take positive action to avoid it!)

The biggest issue is still that the interface of the type depends on the type parameter. It is not determined until T is bound, which means that almost all static checking of the class itself is impossible.

It's not possible to implement such a class with anything but noSuchMethod, and you probably won't be allowed to declare any members, because they might conflict with the actual T type. Example:

class ProxyWithDelay<T> implements T {
  final Duration milliseconds;
  final Object? Function(Invocation) _handler;
  ProxyWithDelay(this._handler, this.milliseconds);
  Object? noSuchMethod(Invocation invocation) {
    sleep(milliseconds);
    return _handler(invocation);
  }
}

That looks weird, but it is what it is.

Then it's used as ProxyWithDelay<DateTime>((Invocation i) { ... }, Duration(milliseconds: 25));.
The problem is that the type ProxyWithDelay has two declarations of get milliseconds, one returning int and one returning Duration. That's an inadmissible conflict, and the type is not allowed to exist.
But we don't necessarily know that, if it's created by an intermediate

dynamic proxy<T>(Object? Function(Invocation) handler) => 
    ProxyWithDelay<T>(handler, Duration(milliseconds: 25));
// ...
  dynamic p = proxy<DateTime>(...);
  DateTime date = p;
  ProxyWithDelay proxy = p;
  int ms = date.milliseconds;
  Duration delay = proxy.milliseconds;

That code satisfies all the stated requirements of the classes involved, and the proxy function doesn't know statically what T will be bound to, and also doesn't restrict which T it can be called with.
But date.milliseconds will either reach the getter returning Duration, which is unsound, or nobody will be able to use milliseconds safely, it'll have to always throw.

So the proxy class is not allowed to declare any members. It's not allowed to inherit any members.
(Could think that private members are OK, but you can also break those by instantiating with a type from the same library.)

So we're back to a class declaration with no superclass, no other implemented types, and no members.
That's not a class.

So imagine instead that we had a function:

external T dynamicProxy<T>(dynamic Function(Invocation) invocationHandler);

which returns a proxy object implementing T, for any implementable interface type T.
Won't accept a base, final or sealed type, it will throw for those, or any other type that a class can't implement.

Every member will have to be implemented using the invocation handler (which is a glorified user-provided noSuchMethod.)

That sound much more likely as a way to introduce a proxy than implements T.

@AdamRose66
Copy link
Author

Having read this through quite a few times, I think I get it.

The reason that

class MyProxy extends Proxy<MyInterface> implements MyInterface

is ok is that the interface is known at this point, and so Dart can do the checking it needs to do to see if this is a well-formed class.

As you might have guessed, I come from a C++ background where

template<type IF> class Proxy : public IF { ... private: IF *m_if; };

is fine ( although we lack the introspection capabilities to do the forwarding to the interface automatically ).

But I assume this is because the template expansion is done during run time in C++ ?

Will the meta-programming stuff allow us to create a macro that creates MyProxy ?

@munificent
Copy link
Member

As you might have guessed, I come from a C++ background where

template<type IF> class Proxy : public IF { ... private: IF *m_if; };

is fine ( although we lack the introspection capabilities to do the forwarding to the interface automatically ).

But I assume this is because the template expansion is done during run time in C++ ?

It works in C++ because template instantiation happens before type checking. A template in C++ really is a template: it's a piece of unprocessed syntax that doesn't become real compilable C++ code until after template arguments are applied. This makes templates very powerful, but it also leads to C++'s famously inscrutable compile errors.

Dart, like most newer languages with generics, has taken a different approach where generic code can be type checked before instantiation using bounds, traits, or some other constraint mechanism.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

6 participants