#1776 Proposal: Enhanced Constructors

brian Thu 16 Feb 2012

This proposal introduces a set of features designed to work together to solve some of the many problems associated with Fantom constructors.

Problem

Summary of problems we wish to solve:

  • the special Foo() syntax is only available to methods named make and fromStr
  • static methods used as factory constructors create problems because they inherit into the namespace of subclasses (causing problems with the "good" name)
  • serialization requires a no-arg constructor called make which burns you single "good" constructor for shortcut syntax
  • auto-generation of boilerplate it-block constructor

Lets consider a simple example:

class A { static A make() { ... } }
class B : A {}

In this case A defines a static factory method called make. But since static methods inherit into B, it means that we cannot define a B.make method. This forces come up with another name for the B constructor, which often is lame and means we can no longer use the good syntax for creating instances of B.

Summary of Changes

This proposal includes the following changes:

  1. enhance default auto-generated constructor to be the following (keeps auto-generation rules simple, but now works with const classes)
    make(|This|? f := null) { if (f != null) f(this) }
  2. introduction of static constructors annotated with static new keywords which are treated as factory methods
  3. enhancement of construction expression syntax to work with any method marked with new keyword (instance constructor or static constructors)

Construction Expressions

Today we overload by parameter type the expression Foo() to map to either Foo.make or Foo.fromStr methods. We also overload operators by parameter type. In this proposal we open up all constructors annotated with the new keyword to be called using the shortcut syntax:

class Foo
{
  new make() {}
  new makeInt(Int x) {}
  new makeStr(Str x) {}
}

Foo()    =>  Foo.make()
Foo(3)   =>  Foo.makeInt(3)
Foo("x") =>  Foo.makeStr("x")

Of course with parameter overloading we might introduce ambiguity, in which case you would have to switch to the named version of the constructor. Note because every constructor still has a unique name we still have clean reflection and dynamic dispatch.

Static Constructors

One thing that I really like about Fantom is that from a "client" perspective there is no difference between an instance constructor and a static method that acts like a constructor:

static Foo make(Str x) {...}
new make(Str x) {}

In both of these cases I call them the exactly same way:

Foo.make(x)
Foo(x) 
type.make([x])
type.method("make").call([x])

But we don't necessarily capture developer intent when using a static method as constructor factory. So I propose to introduce a formal annotation for these factory methods by combining the new and static keywords:

class Foo
{
  static new make(Str x) { FooImpl(x) }
}

They would work just like any static method, but with a few special aspects:

  1. They are implied to return declaring class, in case above it is implied to return Foo?
  2. According to existing rules, methods flagged with new keyword are not inherited into subclass namespace. So they will not conflict with methods in subclass namespace. This solves a lot problems people are having with factory methods on base classes and mixins.
  3. Any method flagged with new (either static or instance) gets to take advantage of shortcut syntax. We now have a way to know that a given method is intended to be used for construction

Breaking Changes

These are the breaking changes this proposal would entail:

All fromStr methods would have to be changed to use new syntax (to take advantage of shortcut syntax)

// old version
static Int? fromStr(Str s, Int radix := 10, Bool checked := true)

// new version
static new fromStr(Str s, Int radix := 10, Bool checked := true)

For next build, I think suggest add warning to any method fromStr not marked as new and implicitly add the new flag. This will keep most things working until code can get updated.

Same goes for static make methods.

The other breaking change would be that in some cases the construction expression might result in ambiguity errors due to ambiguous parameter overloading (since now all constructors will be available). I don't think this will happen too much in practice, and its easy fix to just use the actual constructor name.

Andy and I spent quite a bit of time evaluating different designs. I really dig this design because it combines the idea of static methods and constructor methods quite elegantly with other rules such as inheritance and construction expressions. Please let me know what you think. I'm shooting to add these changes into the upcoming Fantom build sometime late next week.

qualidafial Thu 16 Feb 2012

+1 with one change:

As mentioned in #1775, the function argument to the default constructor should be mandatory if any fields on the object need to be explicitly initialized. i.e. const fields or non-nullable fields.

This gives the compiler a chance to bark at you for failing to provide an initializer when static analysis can prove it's needed.

qualidafial Thu 16 Feb 2012

So basically we're using the new keyword to tackle constructor shortcuts, similar to how we used the @Operator facet to tackle operator overloading?

alexlamsl Thu 16 Feb 2012

Would you mind giving an example to illustrate the difference between new and static new?

I am a bit confused as to why new is not inherently static...

kevinpeno Fri 17 Feb 2012

I'm confused why the different overloads of new need to be named differently. Why can't fantom just support overloading?

Edit: nvm, I just found this

Yuri Strot Fri 17 Feb 2012

This is amazing change!

@alexlamsl constructor is not just a usual method because it hasn't return value and it should initialize object state. However from the user perspective object creation looks like call to static method. And sometimes it's useful to have factory static method instead of constructor. But any static method always delegates to some constructor.

KevinKelley Fri 17 Feb 2012

Really like this proposal, along with #1775, it answers a lot of niggling little issues I've had. +1.

Unifying constructors and static factories, with new and static new, seems like the kind of thing that's so obvious you wonder why it wasn't always that way. Big fan.

lbertrand Fri 17 Feb 2012

Really like this as it addresses some of the annoyance encountered when using Fantom...

A comment though:

  • to overload constructors, we use the new keyword. To overload operators, we use a facet @Operator. Seems inconsistent to me: a facet in one case and a keyword in another case... I feel that transforming the facet to a keyword will not hurt.

And a question:

  • Is the with function still needed with this proposal? I can see the with function just useful for a copy constructor of non const classes, is this correct?

qualidafial Fri 17 Feb 2012

@lbertrand: with is still needed. The |This| constructor is only the default, and can be overridden with an explicit constructor that doesn't take a Func.

tcolar Fri 17 Feb 2012

+1

lbertrand Thu 23 Feb 2012

Any comment on my remark about inconsistency between operator overload and constructor overload?

to overload constructors, we use the new keyword. To overload operators, we use a facet @Operator. Seems inconsistent to me: a facet in one case and a keyword in another case... I feel that transforming the facet to a keyword will not hurt.

brian Thu 23 Feb 2012

Any comment on my remark about inconsistency between operator overload and constructor overload?

They are similar in that they are two cases where we allow overloading. But I also think they are two very beasts. Operators are using symbols and are really mapped using special prefix naming rules. Constructors are designed to look like factories, don't use symbols, and don't work within the prefix naming bounds. Plus there are tons of extra special things we do with constructors such as statement checking, etc. This was the whole reason that using the @Operator facet I original proposed didn't pan out.

brian Thu 23 Feb 2012

Promoted to ticket #1776 and assigned to brian

brian Thu 23 Feb 2012

Ticket resolved in 1.0.62

This work has been completed minus the changes to the auto-generated make constructor. I'm very happy with how it turned out now that I've gotten a chance to see how it works across all our codebases.

I found only 2 or 3 ambiguity errors in Fantom codebase, and about a dozen in our commercial codebase (all cases were ambiguity due to auto-casting and things probably deserved to be clarified anyhow).

lbertrand Thu 23 Feb 2012

I understand behind the scene the 2 are different - but this is a problem of the compiler... And the facet is just an indicator, do not carry any extra information here.

For a developer writing a class, I feel that both approach should follow the same convention and I think moving toward a specific keyword to overload operators is closer to the new keyword to overload constructors instead of using a facet in one case and not in the other...

I can't explain why but to me a facet should carry different/extra information than something which is part of the language and the compiler... It feel to me here than we are going the Java way which is to use annotations for all and everything!

So instead of this

class Foo
{
  new make() { ... }
  @Operator Int plusInt(Int x) { ... }
  @Operator Float plusFloat(Float x) { ... }
}

I will prefer

class Foo
{
  new make() { ... }
  operator Int plusInt(Int x) { ... }
  operator Float plusFloat(Float x) { ... }
}

This is just an opinion and I can live with it - this is more cosmetic than an issue to be resolved... But I think it will keep the language very nice!

brian Fri 24 Feb 2012

I thought you were arguing that constructors should use a facet. I can't argue that it might be nice to make operator a keyword. But taking keywords is a really severe thing that steals that identifier from all programs and make interaction with Java libraries that use that identifier awkward. Facets pollute the type namespace, but seem much less severe in their name stealing (especially with using as). So I think in general any feature like operators used on extremely few classes doesn't deserve a keyword.

Login or Signup to reply.