#1775 Auto-inject makeFunc(|This| f) constructors

qualidafial Thu 16 Feb 2012

When writing const data structure classes I often find myself having to write the same boilerplate over and over:

new makeFunc(|This| f) {
  f(this);
}

I propose that when a class has no constructors declared, and has any non-nullable or const fields, that the compiler inject the above constructor in lieu of the standard no-arg, no-op constructor.

That way this class:

const class Point {
  const Int x
  const Int y
}

Would be desugared by the compiler to act like this:

const class Point {
  const Int x
  const Int y

  new makeFunc(|This| f) {
    f(this)
  }
}

brian Thu 16 Feb 2012

I think the problem is that only makes sense for const classes. In general if you have an auto-generated make method, then you can use with to achieve the same thing:

class Foo { Int x }

// get this for free
Foo { x = 3 }

// because it compiles into
Foo.make.with |it| { it.x = 3 }

The problem is that doesn't work for const classes. But at the same time it doesn't seem like a good idea to be having different rules for what we auto-generate.

This is whole topic also is related to #1370 and also discussions along the lines of how Scala (and now Kotlin) handle constructor/fields too.

qualidafial Thu 16 Feb 2012

This is whole topic also is related to #1370 and also discussions along the lines of how Scala (and now Kotlin) handle constructor/fields too.

The point of this ticket was to break out my suggestion to auto-generate the |This| constructor from the rest of the discussion on #1370.

Depending on the outcome of that discussion, this proposal might need to use the default constructor name of make instead of makeFunc.

I think the problem is that only makes sense for const classes. In general if you have an auto-generated make method, then you can use with to achieve the same thing:

So let's look at what we'd have to do to use with to initialize const objects.

First, our auto-generated constructor needs some sensible defaults for each field:

const class Address
{
  const Str addr1
  const Str? addr2
  const Str city
  const Str state
  const STr zip

  new make() {
    // uh..
  }

}

Next, we need to override with to implement a copy-and-modify strategy:

const class Address
{

  ...

  override This with(|This| f)
  {
    return makeFrom(this, f)
  }

  new makeFrom(This orig, |This| f)
  {
    this.addr1 = orig.addr1
    this.addr2 = orig.addr2
    this.city = orig.city
    this.state = orig.state
    this.zip = orig.zip
    f(this)
  }

}

Now our expression runs as expected:

addr := Address {
  addr1 = "123 Main St"
  city = "Beverly Hills"
  state = "CA"
  zip = "90210"
}

However along the way we had to create a throwaway const object with likely to be bogus default data.

The bottom line is that the signature of the with method (where the function accepts a This but returns Void) is just not compatible with immutable objects.

The problem is that doesn't work for const classes. But at the same time it doesn't seem like a good idea to be having different rules for what we auto-generate.

So how about a really crazy proposal: change all default constructors to the following:

new make(|This|? f)
{
  f?.call(this)
}

This approach carries some side effects:

  • Many/most constructor declarations can be omitted
  • A bunch of compiler errors about uninitialized non-null or const fields would be silenced, and replaced by runtime errors whenever a client caller neglects to initialize any fields.
  • Authors of virtual classes have to be extra careful about whether to explicitly declare a constructor, and think about whether to pass the |This| f argument to the super constructor, or to call super.make(null) and invoke the function in the subclass.

So what does everybody think of these trade-offs? Are there any other issues that haven't been mentioned yet?

brian Thu 16 Feb 2012

So how about a really crazy proposal: change all default constructors to the following: new make(|This|? f) { f?.call(this) }

I think this is the right design and have proposed it along with many other changes. But we can use this specific topic to discuss this aspect.

qualidafial Thu 16 Feb 2012

Addendum:

If any fields const or non-nullable fields have no default value, then the signature would be:

new make(|This| f) // f not nullable
{
  f(this)
}

Otherwise if all fields are nullable or have default values, the signature would be

new make(|This|? f)
{
  f?.call(this)
}

brian Thu 23 Feb 2012

I'm still not quite about this, so I'm going to hold off adding it immediately. I'm debating if saving that one line boilerplate really outweighs the errors you would get if you didn't realize you had non-initialized non-nullable fields. Plus this is really one one small part of an overall problem with boilerplate "struct like" classes.

Login or Signup to reply.