Sdk: Be able to closurize constructors

Created on 14 May 2013  路  34Comments  路  Source: dart-lang/sdk

It would be nice to be able to closurize constructors. I find myself writing a de-serialization for DateTime that needs to be of the form

if (isUtc) {
  return new Date.utc(<long parameter list...>);
} else {
  return new Date(<identical long parameter list...>);
}

It's not that big a problem, but it would be nice to be able to write the parameter list once. There are some obvious questions. How would you refer to the default constructor? Could you provide type arguments? Is it an issue that invoking a closure might implicitly be doing a "new"?

area-language core-l type-enhancement

Most helpful comment

Now that new is optional it changes the way the syntax feels.

Since new is still a keyword we're already not allowed to define a named constructed as new, so ClassName.new tearing off the default constructor shouldn't be breaking, and makes named constructor tearoffs look like static method tearoffs.

All 34 comments

_Set owner to @gbracha._
_Added Accepted label._

How would you refer to the default constructor?

new Foo

The obvious syntax for this to me is to just look like a constructor invocation without any (). That's what closurized method calls look like. So:

  new Foo(1, 2, 3);

would do the same as:

  var closure = new Foo;
  closure(1, 2, 3);

_This comment was originally written by @seaneagan_


I like it! Only down side is that "new " is redundant for named constructors.

Consider:

new Foo
new Foo.bar

vs

Foo.new
Foo.bar

_This comment was originally written by @stevenroose_


What's the chance that this will make it into the codebase anytime soon?

I'm working on a library that may benefit from closurizable constructors. So it this change will eventually come through, I would pause a portion of the implementation and focus on another part first. If it doesn't, I'll need to find an alternative solution.

It's not likely to be very soon, since we are trying to keep things stable. I believe it will come through, but it may be quite a long time.

_This comment was originally written by @stevenroose_


Will this change cause instability then? It shouldn't break anything.
Closurizing regular methods and static methods is already possible, how
different is a constructor from a static method?
But I understand it can take a while, I'll be patient :-)

_This comment was originally written by @seaneagan_


My use case:

https://github.com/seaneagan/unscripted/issues/15

In unscripted, I would like to uniformly use closures to model command-line script behavior. The closures are annotated with command-line metadata, but in the case of sub-commands the methods are usually constructors. So my users would need to create a dummy closurizable factory method which delegates to the constructor and has the metadata. I imagine there are other scenarios where the metadata would actually have to be duplicated on manually created constructor closures, along with the entire constructor signature (including the return type, i.e. the class being constructed), and refactoring tools would be of no help when the constructor changes.

_This comment was originally written by @seaneagan_


I saw a closurization operator [1] was added to ClassMirror, but it appears to not support constructors, and doesn't specify whether the returned closure's function [2] is the actual method declaration which would need to be the case in order to get to the metadata.

_This comment was originally written by @seaneagan_


It's probably obvious, but constructor closures should be canonicalized constants, just like any other static method closures. That would be useful for using them as default factory methods, since default values must be constants.

_This comment was originally written by @seaneagan_


It would be nice if this could support type arguments:

var fooFactory = new Foo<int>;
var barFactory = new Bar.baz<String>;

_Removed Type-Defect label._
_Added Type-Enhancement label._

Issue #17907 has been merged into this issue.

Just a note that we are looking at this again.

_This comment was originally written by @stevenroose_


Nice to hear that! I have ((x) => new X(x)) code blocks everywhere.

_This comment was originally written by @kaendfinger_


See @­gbracha's proposal here: https://github.com/gbracha/generalizedTearOffs/blob/master/proposal.md

I guess https://github.com/gbracha/metaclasses/blob/master/proposal.md is supposed to lead to another solution (as reference for people finding this issue first).

I think this can be closed.

void main() {
  var constructor = new SomeClass#someConstructor;
  var x = constructor();

  print(dateConstructor(isUtc: true)(2000,1,1));
  print(dateConstructor(isUtc: false)(2000,1,1));
}

Function dateConstructor({bool isUtc}) {
  if(isUtc) {
    return new DateTime#utc;
  } else {
    return new DateTime#;
  }
}

class SomeClass {
  SomeClass.someConstructor() {
    print('constructing...');
  }
}

runs fine, just the analyzer still shows warnings for #.

output:

constructing...
2000-01-01 00:00:00.000Z
2000-01-01 00:00:00.000

  var constructor = new SomeClass#someConstructor;
  var x = constructor();

This doesn't work anymore. At least not in the dartpad and on my project.
Any explanation ?

@rrousselGit that feature was removed a short time after it was added. They'll try to find a better approach sometimes after Dart 2.0

Now that new is optional it changes the way the syntax feels.

Since new is still a keyword we're already not allowed to define a named constructed as new, so ClassName.new tearing off the default constructor shouldn't be breaking, and makes named constructor tearoffs look like static method tearoffs.

Any plan on this now that dart 2.0 is out for some time?

so ClassName.new tearing off the default constructor shouldn't be breaking, and makes named constructor tearoffs look like static method tearoffs.

Following that logic, default is probably a better option than new

I have a plan! It's still mostly in my head, but it exists.
It requires allowing the id<typeargs> syntax in more places. When we do that, a lot of things become possible:

  • Denoting a constructor: List<int>.generate
  • Denoting a specialized generic function: T id<T>(T x)=> x; var intId = id<int>;
  • Denoting a specialized generic type: Type listOfInt = List<int>;

I hope we can use the context type to figure out whether something like List<int> or Symbol denotes the type or the default constructor. If the target type is a function type or Function, it's the constructor, otherwise it's the Type. That avoids having to add more syntax that isn't actually related to the constructor.

As a user of the language, when I need to get a constructor as a function, I would very much prefer to put it bluntly:

List<int>.generate as Function;
Map<String, String>.new as Function; // ".new" or ".default" - doesn't matter
//etc.

This kind of expression should be special-cased by a compiler, but it's as intuitive as it gets IMO.

Just want to throw in my $.02:

Is there any particular reason why Date itself can't be used for the default ctor function reference? AFAIK there aren't any situations where there'd be ambiguity between referencing the Date type and Date's default ctor, and it'd mimic the call syntax much more closely, without needing an odd special syntax for it:

if (isUtc) {
  d = Date(...);
} else {
  d = Date.utc(...);
}

// becomes
final ctor = isUtc ? Date : Date.utc;
d = ctor(...);

I'm not at all familiar with how the Dart compiler works internally, but from what I know of compilers _in general_ it's definitely possible to say, "Hey, that typename is being used as an expression, so it _has_ to mean the default constructor".

Am I missing an edge case where there _is_ ambiguity?

It's unlikely that final ctor = isUtc ? Date : Date.utc; can be made to work, mainly because the type system cannot guess what you mean.
The Date expression is evaluated without any type hint, then the result of that is combined with the Date.utc constructor tear-off result, which is likely a function type.
If Date has type Type, then even if it's a callable object, the type of ctor will still be Object.

In Dart 2, class instances are not functions. To make Date useful as both a Type object and function-typed tear-off of the constructor, the same object would have to both be a function and implement Type. That's very confusing to the type system. We might be able to make Date a Type object that is also callable (but I'm not sure what the run-time type of that would be,)

I think it's more promising to let the meaning of Date depend on the context type. Then you would have to write:

Function ctor = isUtc ? Date : Date.utc;`

to give the type hint needed to distinguish Date-as-a-Type from Date-as-a-constructor.

@lrhn Whoops, I forgot that the type is, itself, an object (i.e. final foo = Date is valid, and gives you the RTTI for Date). Call it a C++ hangover.

Over the past year working with Flutter. I've found several times I would like to refer to a constructor function. What was the problem with the old Someclass#someconstructor reference? Is this issue actually scheduled to be worked on?

You can always create a closure for that () => Date(), () => Date.utc()

This is a nice trick, but it solves only the trivial variant of problem. In the title of the issue, we have

if (isUtc) {
  return new Date.utc(<long parameter list...>);
} else {
  return new Date(<identical long parameter list...>);
}

Not sure which case is more common though. Maybe constructors with identical long parameter lists are rare? If so, spending time on it would have very low ROI.

That's one of the reasons why I think spread operator for classes would be lovely.

() => Foo() is fine, but if it has tons of arguments, then it's not anymore.

I assume tear-offs for constructors and getters/setters are still planned.
Closures are only a workaround until it's done.

I think we should also consider the utility in functions like map as another case. For example, let's say you make a Flutter widget FancyText which renders text... fancily. You have an array of strings to put into a Column so they all render in a nice, er, column. Right now you'd have to write:

Column(
  children: textArray.map((s) => FancyText(s))
)

With this, you could write

Column(
  children: textArray.map(FancyText.new)
)

That's obviously a trivial example, but it is a (very nearly) real one that I recently had to use, and in my experience with other languages -- typically more functional ones, though not exclusively -- it's _very_ common. Letting ctors be called like any other function taking arguments and returning a FancyText would be useful.

In similar situations we've moved an SDK issue to the language repository, but in this case https://github.com/dart-lang/language/issues/216 contains a more recent version of the same proposal, so I'll close this one. Please go there for additional discussion!

Was this page helpful?
0 / 5 - 0 ratings