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

Constructor tear-off syntax discussion. #1564

Closed
lrhn opened this issue Apr 7, 2021 · 19 comments
Closed

Constructor tear-off syntax discussion. #1564

lrhn opened this issue Apr 7, 2021 · 19 comments
Labels
constructor-tearoffs question Further information is requested

Comments

@lrhn
Copy link
Member

lrhn commented Apr 7, 2021

A way to tear off constructors as functions, like you can for all kinds of instance methods and function declarations, would be nice. One request for it is #216, but there are more.

The perfect syntax for tearing off a named constructor is ClassName.constructorName, just like you would write the tear-off of a static function declaration.
The perfect syntax for tearing off an unnamed (empty-named) constructor, one invoked as ClassName(args), would seem to be ClassName, but that syntax is already used as a Type literal.

Unnamed constructors

Taking the syntax.

We can, technically, change the syntax for type literals to something else (Foo.class, Type.of<Foo>, ~~Foo, or something), force everybody to migrate to that syntax, and then use the unqualified name as the constructor tear-off.
That's unlikely to be viable in practice. A lot of people are very used to writing type literals as literals, and a lot of code will need to be migrated, even if it's completely automatable.

Probably not viable.

Sharing the syntax.

We could make the context type decide whether Foo means the Type object or the constructor tear-off, just like we do for callable object call method tear-offs.
It's overloading and likely to be confusing. For callable object call method tear-off, no doing the tear-off immediately is not necessarily an issue, if the object keeps having the callable-class type, you can always tear the method off later.
A Type object is not itself callable, so we would need to get the tear-off right at the literal, not when it's later used in a callable context. That makes it much more fragile and likely to give the wrong result. As such, probably not viable either.

Make the Type object callable then?

That'd be a big change. Currently all Type objects have the same type, the opaque Type type.
In order to be able to later tear off a constructor from a Type object, we need a subtype corresponding to the class itself (which then needs a name too).
Say the (meta-)type of the Foo type is Foo.class. Then you can write Foo.class x = Foo;. That object implements Type and is callable. But then, why doesn't it have all the constructors and static members? We'll have introduced proper meta-classes just to tear off a constructor.
Not viable as a solution for something which is a smaller issue than introducing metaclasses.

Separate syntax.

If we introduce new syntax just for unnamed constructor tear-offs, the current proposals are:

  • Foo.new
  • new Foo
  • (Foo.) (slightly tongue-in-cheek, but it could work).

Any other ideas?

The Foo.new syntax suggests that you should also be allowed to invoke the constructor as Foo.new(args), which you currently can't. Can you declare the constructor as Foo.new(...), and Foo(...) is just a shorthand? That might be useful if we want to introduce generic constructor (we do, #1510), then you can declare Foo.new<T>(...) as generic without it looking like a type argument on Foo.

The new Foo syntax suggests that you can also invoke the constructor as new Foo(args), which you can. It's just that we're generally recommending that you don't, so it feels weird reintroducing it as mandatory here.

Instantiated tear-offs.

A class can be generic. You can invoke a constructor as Foo<int>.bar(). Should you also be allowed to tear it off as Foo<int>.bar?

If not, we can allow implicitly instantiated tear-offs like we do for generic functions:

T id<T>(T value) => value;
int Function(int) intId = id;  // Instantiated tear-off, but you can't *write* `id<int>` as an expression.

So there is precedence for only allowing implicit instantiation, and we can do the same for constructors.

List<int> Function(int, int Function(int)) fill = List.filled;
// but not
var fill = List<int>.filled;

But the latter is so much shorter. Because you have to write the entire function type just to specify one part of it, you can end up writing a lot more to get correct implicit instantiated tear-offs.

What about the unnamed constructor? All of Foo<int>.new, new Foo<int> and (Foo<int>.) are new syntaxes that would otherwise be invalid, so they can work.

Should there be explicitly instantiated tear-offs: Set<int>.new, List<int>.filled?
If so, should we make List<int> a valid type literal while we are at it? (#123).

@lrhn lrhn added question Further information is requested constructor-tearoffs labels Apr 7, 2021
@leafpetersen
Copy link
Member

A class can be generic. You can invoke a constructor as Foo<int>.bar(). Should you also be allowed to tear it off as Foo<int>.bar?

I would broaden this question a bit. Specifically, given a generic class Foo<T> with named constructor bar which takes no arguments:

  • Can you tear off bar using Foo.bar? And if so, does it mean:
    • "tear off bar from Foo" (i.e. instantiate to bounds), yielding a value of type Foo<dynamic> Function()
    • or "tear off bar from Foo" , yielding a value of type Foo<T> Function<T>()
  • Can you tear off bar using `Foo.bar?

In other words, we can allow either or both of Foo.bar and Foo<int>.bar, and for the former, we can take one of two interpretations (instantiate to bounds, or tear off a generic).

@mateusfccp
Copy link
Contributor

Separate syntax.

If we introduce new syntax just for unnamed constructor tear-offs, the current proposals are:

  • Foo.new
  • new Foo
  • (Foo.) (slightly tongue-in-cheek, but it could work).

Any other ideas?

Wouldn't it be possible to disambiguate Foo. without the parenthesis? Currently, Foo. is simply a syntax error, so wouldn't it be possible to don't consider it a compile error and instead consider it as the tear-off?

Instantiated tear-offs.

A class can be generic. You can invoke a constructor as Foo<int>.bar(). Should you also be allowed to tear it off as Foo<int>.bar?

Unless there are clear technical reasons for don't allow it, I think this is the clearest option.

@jcollins-g
Copy link

jcollins-g commented Apr 7, 2021

@srawlins pointed out to me that dartdoc already made a syntax choice here long ago, allowing dartdoc references to unnamed constructors as [new Foo]. dartdoc needed to have a way to allow references to the documentation for unnamed constructors as those do have their own pages. A simple example: https://api.dart.dev/stable/2.12.2/dart-core/Comparable/Comparable.html

Dartdoc is also making a subtle assumption here in the output file structure that the unnamed constructor is named the same as its defining class. Dartdoc complains very loudly (fails with error by default) if you somehow have or inherit a member named the same as a class it is contained in for this reason. Fortunately, people don't seem to do this in practice, so another option you could consider might be unnamed constructors with an implicit name the same as the defining class.

The unnamed constructor then references as: Foo.Foo and tearoffs work the same as for static methods. Dartdoc also allows this as a way to reference unnamed constructors.

(edit: Not as clear to me that this would have a broad impact after thinking about it some more.). That could have some big ripples in the language specification, and there may be subtleties I don't get here which make this a horrible notion. As a matter of practice though, I'd be surprised if this caused any ecosystem disruption to change because dartdoc already does enforce this.

I don't otherwise have a strong opinion. These are just the prevailing winds from dartdoc's perspective.

@Levi-Lesches
Copy link

I know it's not a strict issue, but Foo.Foo is a tad akward to read (consider LayoutBuilder.LayoutBuilder or MyStateNotifier.MyStateNotifier).

I'm personally in favor or Foo.new, since new is a reserved keyword anyway (so no breaking changes) and it correctly conveys the notion that it creates a new instance.

@Levi-Lesches
Copy link

Levi-Lesches commented Apr 7, 2021

Are you suggesting this?

class Foo<T> {
  Foo.bar(T value);
}

Foo<T> Function<T>(T) a = Foo.bar;
Foot<T> b = a<int>(42);

Because, to your point, this is how you would currently do cast's tear-off:

List<num> x = [1, 2, 3];
List<R> Function<R>() y = x.cast;
List<int> z = y<int>();

@lrhn
Copy link
Member Author

lrhn commented Apr 7, 2021

The issue with

  • or "tear off bar from Foo" , yielding a value of type Foo<T> Function<T>()

is that it doesn't interact well with generic constructors (which is another feature we do want, #1510).

If you can write

class Foo<T> { 
  Foo.baz<R>(Iterable<R> elements, T convert(R)) { ... }
}

to declare a generic constructor (on a class which is generic or not), then tearing off that Foo.baz should yield a function of type Foo<T> Function<R>(Iterable<R>, T Function(R)) for some T, likely either Foo instantiated to bounds or inferred from the context type (the same way we infer the type arguments when calling the constructor, just based on the return type of the function-typed context type).

So, making Foo.bar tear off effectively <T>(args) => Foo<T>.bar(args), will make tearing off Foo.baz inconsistent. It can't be <T><R>(args) => Foo<T>.bar<R>(args).

It can be <T, R>(args) => Foo<T>.bar<R>(args), if we are willing to combine the type parameter lists. Then it becomes an issue whether we can choose whether to do this, or instantiate T, R, or both, when tearing off baz.

I still we risk being inconsistent, or at least confusing, and would prefer if we always instantiate the class when we tear off a constructor.

@Levi-Lesches
Copy link

Levi-Lesches commented Apr 7, 2021

@lrhn:
To @tatumizer's point, maybe we solve this by doing:

class Foo<T extends num> {
  Foo.bar<R>(T value1, R value2);
}

Foo<int> Function<R>(int, R) tearOff1 = Foo<int>.bar;
Foo<num> Function<R>(num, R) tearOff2 = Foo.bar;

So basically refer to the Foo for T and instantiate to bounds otherwise (if I'm using the phrase correctly).

@lrhn
Copy link
Member Author

lrhn commented Apr 7, 2021

@tatumizer A const constructor is just a normal constructor when used at run-time. The fact that it can be called using const means nothing when it isn't.

A more interesting thing is that a constructor tear-off can be a constant function value if the class is not generic, and maybe if the class is generic (depending on whether the instantiation type is constant).

@munificent
Copy link
Member

munificent commented Apr 7, 2021

I was hoping we could change the type literal syntax to something else and use the bare class name for the unnamed constructor tear-off. Unfortunately, I don't think that will fly. The Flutter find.byType() method is very common in tests (I see 23,000+ uses inside Google alone). I think that API alone means type literals should probably keep the shorter syntax. (I might wish that the API had been find.byType<T>() instead, but that ship has sailed.)

new ClassName

I think we should not use new ClassName:

  • That is valid C++ syntax and it means to invoke the constructor, so we will horribly confuse some segment of users if we use that to just look up the constructor.

  • Is a constructor tear-off a constant expression? That seems reasonable to me. If so, const new ClassName looks pretty odd to me, but const ClassName.new seems OK.

ClassName.new

I think we should use ClassName.new for the unnamed constructor. My arguments are:

  • Metaclass instance member. If we ever do metaclasses, keeping the class name referring to the type will make it easier (I think) to evolve that to refer to the metaclass. At that point, using .new then becomes a reasonable instance member syntax to refer to an unnamed constructor on a metaclass object:

    constructThing(metaclass) {
      return metaclass.new(); // Instance syntax.
    }
    
    main() {
      // Gets ref to SomeClass's metaclass.
      var metaclass = SomeClass;
      // Pass to fn:
      constructThing(metaclass);
    }

    We obviously don't need to support this syntax initially, but using a method-like syntax instead of something like a prefix new paves the way for an ultimately more consistent language. (For what it's worth, ClassName.new() is how you invoke constructors in Ruby.)

  • Generic constructors on generic classes. We've talked about supporting generic constructors (Constructor specific generics #647). If we do that, it's somewhat reasonable to allow generic unnamed constructors. However, a generic unnamed constructor on a generic class would required a pair of type argument lists, which is syntactically pretty weird. In most cases, the type arguments would be inferred, so the duplicate type argument list isn't a problem:

    class Sequence<E> {
      Sequence<T>(List<T> elements, E Function(T) transform) { ... }
    }
    
    main() {
      var sequence = Sequence(elements, (e) => e.toString());
    }

    But in rare cases where you do need to write a type argument list, this would give you a way to do it:

    main() {
      var upcast = Sequence<String>.new<Object>(elements, (e) => e.toString());
    }

In general, I think postfix high-precedence method-like syntax plays better with the rest of the language. We have often been frustrated by having await be a low precedence prefix expression and wished for a postfix form. I don't think we've ever regretted something being postfix. I like the .new syntax because it looks like you're taking the class and doing something to it (looking up its constructor), which I think is a good mental model.

Wildcard!

Another option is to not add specific constructor tear-off syntax. Instead, do some kind of more general partial application syntax. Here's a strawman:

A ... in an argument list takes the surrounding invocation and wraps it in a closure with parameters for all of the elided arguments in the given argument list. Then the ... is replaced with arguments forwarded from those parameters. So this:

function(a, b, c, d) => a + b + c + d;

var examples = [
  function(...),
  function(1, ...),
  function(1, ..., 4),
  function(1, 2, ..., 4),
];

Is equivalent to:

function(a, b, c, d) => a + b + c + d;

var examples = [
  (a, b, c, d) => function(a, b, c, d),
  (b, c, d) => function(1, b, c, d),
  (c, d) => function(1, b, c, 4),
  (c) => function(1, 2, c, 4),
];

We would then allow this syntax in constructor invocations as well. With this, the unnamed constructor tear-off that forwards all parameters is just:

SomeClass(...)

That's only one character longer than SomeClass.new and generalizes to let you partially apply some parameters, which can be pretty handy.

@munificent
Copy link
Member

The counter-argument could be: if dart implements partial function application, then we will still have 2 methods for tearoff for any function or method: one with old syntax var func=obj.method;, another- with new syntax (e.g. var func=obj.method(...);).

Yeah, this is a good point.

@mit-mit
Copy link
Member

mit-mit commented Apr 9, 2021

I like the idea from DartDoc, usingFoo.Foo. It seems very consistent with named constructors to me. Are there any issues with that?

For consistency, we should then probably make it legal to use that as a constructor call (e.g. var f = Foo.Foo()), and then specify that Foo() is just syntactic sugar for Foo.Foo()?

@Levi-Lesches
Copy link

I like the idea from DartDoc, usingFoo.Foo. It seems very consistent with named constructors to me. Are there any issues with that?

Well, named constructors don't typically begin with a capital letter. Foo.new doesn't have that problem

@munificent
Copy link
Member

munificent commented Apr 9, 2021

It seems very consistent with named constructors to me.

It's not consistent with how unnamed constructors are declared or invoked, which is probably the comparison that matters most.

Are there any issues with that?

Mainly that when the class name is long (which is pretty common) it's likely to be even more verbose than making a closure manually, which users can already do today. I did a really hacky scan over a bunch of pub packages looking for existing closures that could be constructor tear-offs if we supported them. Of the ones I found, 31 would be improved using constructor tear-offs with dartdoc style syntax like Foo.Foo. But 84 would actually be even longer using that syntactic "sugar". Some examples:

// Cases where Foo.Foo is shorter:
_Node._Node
() => _Node()

Address.Address
(a) => Address(a)

StoreScreen.StoreScreen
(childRouter) => StoreScreen(childRouter)

Text.Text
(e) => Text(e)

// Cases where the existing explicit closure is shorter:
_Example._Example
() => _Example()

GlobalKey.GlobalKey
() => GlobalKey()

GetReplicatorPendingDocumentIDs.GetReplicatorPendingDocumentIDs
(address) => GetReplicatorPendingDocumentIDs(address)

ImmediateMultiDragGestureRecognizer.ImmediateMultiDragGestureRecognizer
() => ImmediateMultiDragGestureRecognizer()

AdvanceMsgItem.AdvanceMsgItem
(e) => AdvanceMsgItem(e)

StandardDialogsLocalizations.StandardDialogsLocalizations
(locale) => StandardDialogsLocalizations(locale)

AsyncIoRequestClient.AsyncIoRequestClient
(prefix) => AsyncIoRequestClient(prefix)

IdbStoreRecordSnapshotSqflite.IdbStoreRecordSnapshotSqflite
(row) => IdbStoreRecordSnapshotSqflite(row)

Note that using .new for the tear-off is an improvement over every single one of these existing closures.

@Levi-Lesches
Copy link

Also note that, as unlikely as it is, Foo.Foo is currently a valid named constructor. Foo.new is not, since new has not yet been "released" even though it has been deprecated. I feel we can put it to good use here before letting people use it as an identifier again. The same discussion was had with Foo.default, but the semantics there only work if you're already thinking about named vs unnamed constructors. If you're just thinking "I want to make a new Foo", Foo.default is unintuitive (especially if it has required parameters).

@munificent
Copy link
Member

Also note that, as unlikely as it is, Foo.Foo is currently a valid named constructor.

This too.

since new has not yet been "released" even though it has been deprecated. I feel we can put it to good use here before letting people use it as an identifier again.

I don't expect we'll ever "unreserve" the new keyword. I can't think of any language that's done that before.

@lrhn
Copy link
Member Author

lrhn commented Apr 10, 2021

People seem to be gravitating towards Foo.new. I'll try to update the proposal with this next week.

@lrhn
Copy link
Member Author

lrhn commented May 6, 2021

The "bind"/"curry" operation you describe would partially apply the provided parameters (which must match a prefix of positional parameters and subset of named parameters), and then evaluates to function which expects the rest.
E.g.:

int foo(int x, [int? y, int z = 0]) => ...
int bar(int x, int y, {int? z, int w = 0}) => ...
var f1 = foo**(1, 2);  // ([int z = 0]) => foo(1, 2, z)
var f2 = bar**(1, z: 2); // (int y, {int w = 0}) => bar(1, y, z: 2, w: w)

If we extend that to type parameters, then we'd get:

R baz<R, T>(T x, [T? y]) => ...;
var f3 = baz**<int, int>(); // (int x, [int? y]) => baz<int, int>(x, y)
var f4 = baz**<int>(2); // <T>([int? y]) => baz<int, T>(2, y)

(I put the ** before the type parameters, because if I have that feature, I want to partially apply type arguments too!)

It can express any kind of partial application. You need the ** and () to specify type parameters, but it works.

It doesn't work for constructor tear-offs, though, because the type arguments are on the class, C<int>.named**(). Well, I guess that does work.

The question is what type we base the specialization on: The static type or the run-time type of the function in the partially applied invocation. That is, if we have:

int Function(int) f = (int x, [int? y]) => y ?? x;
var g = f**(2); 

will g be ([int? y]) => f(2, y) or () => f(2)?
If we do want to be able to specialize any function, I think it should be decided at the specialization point, which means using the static type.

Can you specialize a dynamic function? dynamic x = (int x, int y) => x + y; var f = x**(1);. Is this allowed? If it is, what does it do (that's actually easy, dynamic operations always try to do what they would do if the static type of the receiver was its actual run-time type. It's just going to be expensive, so we might want to disallow it).

(Another strawman syntax: foo@<int>(2). I think the "at" reads well 😁 ).

@Levi-Lesches
Copy link

I guess this can be closed?

@munificent
Copy link
Member

I think so. I'll go ahead and close it and let @lrhn re-open if there's something unresolved that I missed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
constructor-tearoffs question Further information is requested
Projects
None yet
Development

No branches or pull requests

7 participants