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)
}
}
brianThu 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.
qualidafialThu 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:
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?
brianThu 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.
qualidafialThu 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)
}
brianThu 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.
qualidafial Thu 16 Feb 2012
When writing
constdata 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
makemethod, then you can usewithto 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
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
makeinstead ofmakeFunc.So let's look at what we'd have to do to use
withto initializeconstobjects.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
withto 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
Thisbut returnsVoid) is just not compatible with immutable objects.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:
|This| fargument 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
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.