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

Construction of generic types "new T()" #30074

Closed
mfrancis107 opened this issue Jul 5, 2017 · 27 comments
Closed

Construction of generic types "new T()" #30074

mfrancis107 opened this issue Jul 5, 2017 · 27 comments
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). closed-not-planned Closed as we don't intend to take action on the reported issue type-enhancement A request for a change that isn't a bug

Comments

@mfrancis107
Copy link

mfrancis107 commented Jul 5, 2017

With strong mode becoming the default in 2.0
I was wondering if it would then be possible to add support for calling constructors based on generic types?

T Create<T>(){
 return new T();
}

An example use case would be.

import 'dart:typed_data';

class VertexList<T>{
  Float32List vertexData;
  int vertexSize;

  VertexList(){
    vertexSize = T.vertexSize;
  }

  T getAt(int i){
    var offset = i * vertexSize;
    return new T.fromView(new Fload32List.view(vertexData.buffer, offset, offset + vertexSize));
  }

}

class MyVertex{
  static int vertexSize = 8;

  Vector3 position;
  Vector3 normal;
  Vector2 uv;

  MyVertex.fromView(Float32List view){
    position = new Vector3.fromView(view);
    normal = new Vector3.fromView(view);
    uv = new Vector2.fromView(view);
  }
}

class NotVertexClass{
}

/// usage
var list = new VertexList<MyVertex>(); // This is good!
var badList= new VertexList<NotVertexClass>(); // Error: NotVertexClass does not have constructor fromView

list.getAt(0).position.x = 10.0;
@floitschG floitschG added the area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). label Jul 5, 2017
@lrhn lrhn added the type-enhancement A request for a change that isn't a bug label Jul 7, 2017
@lrhn
Copy link
Member

lrhn commented Jul 7, 2017

There is the obvious problem, which you also point out, that there is no static way to know which constructors a the type of a type parameter supports.
That can mean either getting constructor errors as runtime errors, with no static checking, or somehow constrain type parameters to classes with the desired constructors (specified in some way).

In C#, you can require a type parameter to have a zero-argument constructor, but you can't pass any parameters. In Dart, that would correspond to only allowing zero-argument unnamed constructors to be used. That wouldn't allow your T.fromView above.

If you have to handle named constructors or constructors with arguments differently anyway, I'm not sure I think the feature is worth it.

@mfrancis107
Copy link
Author

I get your point. I guess you'd have to introduce abstract named constructors? (Not sure if possible) And require T extends SomeAbstract class.

class VertexList<T extends Vertex>{
  Float32List vertexData;
  int vertexSize;

  VertexList(){
    vertexSize = T.vertexSize;
  }

  T getAt(int i){
    var offset = i * vertexSize;
    return new T.fromView(new Fload32List.view(vertexData.buffer, offset, offset + vertexSize));
  }

}

abstract Vertex{
  Vertex.fromView(Float32List view); /// Abstract constructor?
}

class MyVertex extends Vertex{
  static int vertexSize = 8;

  Vector3 position;
  Vector3 normal;
  Vector2 uv;

  MyVertex.fromView(Float32List view){
    position = new Vector3.fromView(view);
    normal = new Vector3.fromView(view);
    uv = new Vector2.fromView(view);
  }
}

@matanlurey
Copy link
Contributor

This would mean potentially retaining constructor information for all Types, which is a major no-no for ahead-of-time compilation that needs tree-shaking and dead code elimination. This should probably be a "wontfix" @lrhn

@lrhn
Copy link
Member

lrhn commented Jul 8, 2017

We definitely do not want type parameters to carry static members in general (constructors are somewhere between static members and instance members). To make that useful, we'd have to make "static interfaces" that abstracts over a number of static classes, and then the classes basically just become a bunch of singleton objects.
Even doing it just for nameless parameterless constructors seems like more complication than it's worth. If you need something to abstract over the ability to create objects, you can just make it accept a factory function as argument along with the type parameter.

@vsmenon vsmenon added the closed-not-planned Closed as we don't intend to take action on the reported issue label Aug 31, 2018
@vsmenon
Copy link
Member

vsmenon commented Aug 31, 2018

Closing per the above comments.

@vsmenon vsmenon closed this as completed Aug 31, 2018
@nathanfranke
Copy link

nathanfranke commented Jul 24, 2020

It works in C++, and C++ is compiled, so...?

template<class T>
struct Foo {
    static void test() {
        T::some_method();
    }
};

Closing per the above comments.

We're going to close this feature request, even though we have unanimous 13-0 votes on the parent comment?

Edit: #34131 and also a unanimous 0-13 downvotes here...

@lrhn
Copy link
Member

lrhn commented Jul 24, 2020

That's a C++ template, which is quite different from anything in Dart.
C++ expands all templates at compile time, then checks whether the result is valid. You an instantiate this template with any type for which T::some_method() would be valid.

Dart generics are retained at run-time, as run-time parameters of the instances. We still want to statically detect errors, so we can't just allow you to pass any type without somehow ensuring that it will work everywhere that the type parameter flows.
That's why we need interfaces (C++ does not have interfaces), so that we can say that an object satisfies at least that interface, so you can safely use those interface methods.

In order to allow new T we would need the type to satisfy some "type interface", which is a completely new concept.

It's just not a good match for an interface based, run-time generic language like Dart.

@nathanfranke
Copy link

In that case, what about something like this?

class Foo<T extends BaseClass> {
    static void bar() {
        T.baz();
    }
}
class BaseClass {
    static void baz() {
    }
}

If T extends BaseClass then T.baz() must always be valid.

@lrhn
Copy link
Member

lrhn commented Jul 24, 2020

Dart does not inherit static members, so a subclass of BaseClass is not guaranteed to have a static baz member.

@nathanfranke
Copy link

nathanfranke commented Jul 24, 2020

Ah, true. In that case, dart-lang/language#356
I did solve this with reflection, but that's only temporary

@michpolicht
Copy link

C++ does not have interfaces

C++ does not need interfaces, because it allows for multiple inheritance and pure abstract class (possibly with virtual inheritance) serves the same purpose as interface. Moreover "type interfaces" were recently introduced and they are called "concepts". So please do not refer to C++ to justify Dart missing features.

@etegan
Copy link

etegan commented Feb 10, 2022

@james-poulose
Copy link

I ended up creating a factory like this. Full usage here.

class ServiceFactory<T> {
  static final Map<String, dynamic> _cache = <String, dynamic>{};

  static T getInstance<T>(T Function() creator) {
    String typeName = T.toString();
    return _cache.putIfAbsent(typeName, () => creator());
  }
}

@michpolicht
Copy link

Dart generics are retained at run-time, as run-time parameters of the instances. We still want to statically detect errors, so we can't just allow you to pass any type without somehow ensuring that it will work everywhere that the type parameter flows

And why runtime error would be wrong? And why C++ can statically detect such errors and Dart can't? What's the problem in checking whether constructor parameters are valid at compile time?

With such attitude of the devs this language is going nowhere.

@eernstg
Copy link
Member

eernstg commented Feb 21, 2022

C++ templates are basically macros. Each application of a C++ template to a list of actual template arguments (inferred or passed explicitly) gives rise to a separate piece of code, which is compiled. This means that any two different template instantiations could give rise to completely different generated code, and also that you can have compile-time errors in the body of a template during compilation of an instantiation.

The former is a major problem because a C++ template is basically a copy-paste mechanism. With 10 different usages of a template A, you may well have a program that contains 10 different copies of the code in A (more precisely: the generated code is bloated because it copies the result of compiling A 10 times separately, with small differences here and there because the actual template arguments are different). A lot of work has been done over the years in order to avoid creating many copies of the same thing, but the code bloat problem with C++ templates is a built-in property of any mechanism which is essentially based on macros. Some more information about this problem is given here.

The second major problem mentioned above is that C++ templates are basically untyped. We just pass some actual template arguments and produce some code (an instantiation of the template), and the type checker doesn't get to look at the code at all before it has been instantiated. (C++ concepts are intended to help with this, and they do help somewhat, but only if you get the requirements right.)

Dart generics is statically typed (if the actual type arguments satisfy the bounds then there will never be a compile-time error in the body of a generic type declaration), and it never duplicates the compiled code of a generic type declaration. I don't think it's likely that a proposal to change Dart to use a macro-like notion of generics would get much support.

So it's more about having a well-defined type parameterization mechanism, and avoiding code bloat, and less about attitudes.

@michpolicht
Copy link

C++ templates are not macros. It's a turing-complete meta-programming language and of course it generates code at compile-time, because that turns out to be useful feature and its true purpose. C++ templates enable techniques such as static polymorphism, while mechanisms such as virtual methods allow you to obtain runtime polymorphism. These are two complementary mechanisms. If you say that this is a "problem" then you don't understand what it is.

If I understand you correctly, then you seem to tell me that in Dart generics basically serve the same purpose as method overriding, which in case of Dart can be with achieved through interfaces or inheritance. So Dart generics are like a competing mechanisms that serves the same purpose. Why would you need that? What makes generics useful is what people in this thread are asking for. We need some meta-programming capabilities, not just alternative syntax to create virtual methods.

@eernstg
Copy link
Member

eernstg commented Feb 21, 2022

It's a turing-complete meta-programming language

Macro expansion can certainly be Turing complete. The untyped lambda calculus is Turing complete, and it is directly based on substitution. The point here is that C++ template expansion is not type checked, type checking only occurs after expansion. (Yes, C++ concepts allow you to specify certain constraints, but it is very different from an actual, sound type checking mechanism.)

Dart generics basically serve the same purpose as method overriding

Generics in Dart (as well as in most other programming languages, C++ is the exception) serve to define type parameters on a type or function declaration D, such that it is possible to obtain generic instantiations of D. Method overriding serves to allow OO dispatch to select a method implementation that is associated with the dynamic type of the given receiver. These two concepts are completely different.

Just a simple example: In a version of Dart without type parameters, how would you use method overriding to achieve a similar effect as the choice between Iterable<int> and Iterable<String> as a type (for instance, as the return type of a function)?

Sure, you could write a special ListOfInt and copy-paste the code and create a similar ListOfString, and they could have a common supertype List, and that one could have a supertype Iterable with subtypes IterableOfInt and IterableOfString, and ListOfInt should have IterableOfInt as a supertype as well as List, and so on and so on. So you may be able to create a (finite and, in practice, small) set of classes with relevant subtype relationships, to compensate for the fact that you can't use type parameters. But I can't see how method overriding would even be relevant to this endeavour. Sure, we would use method overriding and inheritance in all those classes, but it wouldn't help us solving the typing problems; for instance, you couldn't write code that works on a List<X>, because you have no way to abstract over ListOfInt and ListOfString etc., you would immediately have to go back and use the plain List (and give up on type safety).

@michpolicht
Copy link

Don't pick my sentences out of a context.

These two concepts are completely different.

That's what I'm pointing out, do not turn things upside down. It is you who made a claim that C++ is somewhat problematic and there is something wrong about it, while in fact it utilizes the concept of generic programming to the very end.

I'm also not saying that Dart generics are completely useless, but the whole point of using generics is to operate on abstractions like Iterable<T> and if we can't instantiate T then in many cases it really cuts off legs.

You don't need to copy-paste the code to obtain similar results, just use delegate design pattern (to obtain iterator) and perform some type casting. Older versions of Java did something similar with Collection interface. But again, that wasn't my point.

@michpolicht
Copy link

The point here is that C++ template expansion is not type checked, type checking only occurs after expansion

So does it happen or not? Is the type checked after the expansion? If so, then why do you claim that template expansion is not type checked? This looks to me like self-contradicting statements, so I have troubles with understanding you.

@eernstg
Copy link
Member

eernstg commented Feb 21, 2022

It is you who made a claim that C++ is somewhat problematic and there is something
wrong about it, while in fact it utilizes the concept of generic programming to the very end.

Certainly C++ templates are somewhat problematic, because they are not type checked, it is only each instantiation which is type checked (as usual, I should mention that concepts are capable of performing specific checks, but only the ones that you have remembered to ask about, so that's nowhere near the same thing as a static type check of the template).

C++ templates do indeed allow for maximal flexibility when it comes to handling the situation where different template arguments give rise to completely different generated code. In that sense it is optimal: Nothing can give you more flexibility than not checking.

When template instantiation has taken place, the resulting C++ code is type checked as usual.

This makes template instantiation similar to other code generation mechanisms: If the generated code passes the compiler then you're lucky. If the generated code is rejected by the compiler then it's because you are starting from an unchecked entity: The template itself.

Almost all other languages use statically checked type parameterization mechanisms, that is: If the bounds checks on actual type arguments succeed then the resulting generic instantiation is type correct. You never need to type check the body of a type parameterized entity after instantiation. C++ templates do not have that property.

You don't need to copy-paste the code to obtain similar results, just use .. some type casting

I think you made my point here, not yours. ;-)

@michpolicht
Copy link

Certainly C++ templates are somewhat problematic, because they are not type checked, it is only each instantiation which is type checked (as usual, I should mention that concepts are capable of performing specific checks, but only the ones that you have remembered to ask about, so that's nowhere near the same thing as a static type check of the template).
(...)
This makes template instantiation similar to other code generation mechanisms: If the generated code passes the compiler then you're lucky. If the generated code is rejected by the compiler then it's because you are starting from an unchecked entity: The template itself.

And what's exactly problematic with this approach? You get compile time error in both cases, right? And C++ templates are not just glorified macros, they are type-aware. Template specializations are one manifestation of this. SFINAE is another one.

@lrhn
Copy link
Member

lrhn commented Feb 22, 2022

@michpolicht

And why runtime error would be wrong?

The goal of static checking is to guarantee that you won't get runtime errors.
Dart has moved quite some way towards doing static checking, vs. allowing runtime errors.
The Dart 2 type system and the Null Safety change were both aimed solidly at using static checking to prevent runtime errors.
By all counts, our current target demographic wants us to go even further (up to and including, for some people, removing dynamic from the language).

Dart generics are designed to allow the same code to run on different types. The code is generic, it works for all types (or at least all types satisfying the type parameter bounds).
That has some consequences, like not being able to do anything to the type that is not possible for all possible instantiations of the type. That's why you can't do void foo<T extends int>(T x) { x = 2; }, because there are subtypes of int which 2 can't be assigned to (only Never in this case, but one is enough).
And that's why you can't call constructors or static members on T. Not all subtypes need to have those, and Never certainly doesn't.

Since we want to allow:

SomeClass<T> createSome<T>() => SomeClass<T>();

and it's undecidable (in the general case) which, and how many, type arguments can flow into createSome, then we can't possibly do static macro expansion instead. The following code is valid Dart:

abstract class Builder<T> {
  T create();
}
class ValueBuilder<T> {
  final T value;
  ValueBuilder(this.value);
  T create() => value;
}
class ListBuilder<T> implements Builder<List<T>> {
  final Builder<T> elementBuilder;
  ListBuilder(this.elementBuilder);
  List<T> create() => [elementBuilder.create()];
}

Builder nestedList<T>(int n, Builder<T> value) => 
    n <= 0 ? value : nestedList<List<T>>(n - 1, ListBuilder<List<T>>(value));

If I write nestedList<int>(99, ValueBuilder(0)), I'll get 99 different instantiations of Builder, with different types, at runtime.
That's something C++ cannot do.

Imagine a "typing JSON parser", which actually tries to type the collections it builds to match the elements. That's possible in Dart, but again, the type instantiations are created at runtime.

So, Dart's generic functions are not inherently inferior to C++'s macro expansion, it's different. It's similar to C# and Java's generics (apart from the types being remembered at runtime, so we can actually do something like if (value is T)).
The one difference is that C# does allow restricting type arguments to types which has a zero-argument constructor, and then allows you to use that constructor.

@eernstg
Copy link
Member

eernstg commented Feb 22, 2022

@michpolicht wrote:

And what's exactly problematic with this approach? You get compile time error in both cases, right? And C++ templates are not just glorified macros, they are type-aware.

C++ templates are a time-proven language design element in C++, it allows for the maximal flexibility that I mentioned, and it allows for C++ code to remain as low-level and highly performant as we'd expect from C++. So the template design is a fact of life in C++, and C++ software is written to use the properties offered by this design, and it's all good.

But C++ templates are also a leaking abstraction, in the sense that it allows us to define named entities whose internal consistency has not been established. For instance:

template <class T, class U>
T GetMin(T a, U b) {
  return (a<b?a:b);
}

int main() {
  int i = 2;
  std::cout << GetMin(1, &i);
  return 0;
}

If you instantiate this template with types <int, int> or <int, long> or many other specific type arguments then it works. But if you use int and int* then you get an error which is associated with a part of the body of the template declaration, because there is no definition of < with the required type:

tst.cpp: In instantiation of ‘T GetMin(T, U) [with T = int; U = int*]’:
tst.cpp:10:22:   required from here
tst.cpp:5:12: error: ISO C++ forbids comparison between pointer and integer [-fpermissive]
    5 |   return (a<b?a:b);
      |           ~^~
tst.cpp:5:14: error: operands to ‘?:’ have different types ‘int’ and ‘int*’
    5 |   return (a<b?a:b);
      |          ~~~~^~~~~

This means that the template as such is never type checked. Type checking only takes place on each instantiation. (Remember the usual disclaimer: concepts exist, but they are not a type checker.) That in turn means that if you create a library that contains template declarations then you can't know whether they will work with all the type arguments where the template is intended to work, and your customers will not have any knowledge about which type arguments will work and which ones will cause errors somewhere deep in the body of the template.

That's what I mean when I say that the template as such is untyped. It doesn't specify (and hence, of course: doesn't enforce) any constraints on the template arguments, you just have to try to pass some actual template arguments and then it may or may not work.

The core point in this discussion was basically the claim that Dart ought to adopt the core design decisions that were taken for C++ templates, and then we'd be able to do various desirable things. My argument why that wouldn't work well is that "that's not a good fit for Dart, we want typed abstractions".

@michpolicht
Copy link

Sorry, I don't have much time right now, so I will try to make a full reply later. For now I just want to address following point

That in turn means that if you create a library that contains template declarations then you can't know whether they will work with all the type arguments where the template is intended to work, and your customers will not have any knowledge about which type arguments will work and which ones will cause errors somewhere deep in the body of the template.

In both cases: C++ type substitution and "real type checking" you get compile-time error, so I don't understand what is the real problem here. Moreover you can enforce type checks for particular types with explicit template instantiation:

template int GetMin<int, int*>(int, int*);

Yes, there is a problem that template users and implementers had to rely on conventions, but C++ concepts are there to solve this problem and they are richer version of what you can do in Dart with template constraints like: T extends SomeClass.

removing dynamic from the language

Then good look with light-weight storage for incoherent collections like JSON.

@eernstg
Copy link
Member

eernstg commented Feb 23, 2022

C++ type substitution and "real type checking" you get compile-time error,
so I don't understand what is the real problem here.

You get a compile-time error for an actual instantiation where the type arguments fail to satisfy some constraints that are derived directly from every little implementation detail in the body of the template. You can't conclude anything about the correctness of an instantiation of the same template with different type arguments, so you can't promise your customers that your template is correct. You'll have to leave it at "just go ahead and use it, it might work!".

concepts are there to solve this problem

They support the specification of requirements (e.g., T must be a class, it must have a method foo that accepts an argument of type int, etc.), but the author of the template must come up with those requirements. There is no mechanism to ensure that the requirements specified using concepts are correct: They could be too strong, too permissive, or irrelevant, and you'll just have the same possible outcomes as without concepts: Any given instantiation of the template may or may not give rise to compile-time errors in the body of the template. So a template-with-concepts is just as untyped as a template that doesn't use concepts. The only difference is that you might consider a concept failure to be "nicer" than a body failure; but there is no guarantee that the concepts will correctly characterize which actual type arguments will work, and which ones will not. You still have to tell your customers "just go ahead and use it, it might work!".

You may think that this is not a problem, but Dart isn't likely to choose that point in the design space.

@michpolicht
Copy link

With Dart you tell your customer "sorry construction of generic type won't work, because Dart devs think it's not useful/unsafe", so every stick has two ends. And TBH I still don't understand your point, because there's really no value in providing a generic class that works for all types, when only certain type properties are required (and concepts are a formal mechanism to establish it). Failed template substitution acts in the same way as Dart early type checking mechanism, errors are caught at compile-time - it does not make software product buggy and I've never heard about anybody having any real problems with what you describe here.

@eernstg
Copy link
Member

eernstg commented Feb 23, 2022

(I'll stop here, I won't repeat the explanations given previously.)

@dart-lang dart-lang locked as too heated and limited conversation to collaborators Feb 23, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). closed-not-planned Closed as we don't intend to take action on the reported issue type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

10 participants