#292 Constructor Design Take 2

brian Thu 17 Jul 2008

Another long walk tonight to really think about constructors and the current discussions:

Basically all the proposed changes involve trying to simplify the current constructor design and solve the with-block validation problem. I've convinced myself that the with-block validation issue is an orthogonal issue - it is just one or more methods run after construction or deserialization and no proposal really changes that.

If we take that off the table, then all the proposals revolve around how to allocate a new object and do initialization in some type of factory method. The issues involve:

  • you can't overload static methods since they are inherited, so there can only be one make in a class hierarchy
  • how to determine when to allocate the object
  • how to scope initialization code to the new instance
  • how to initialize your super class
  • how to define protection scoping of your constructors (internal, private, etc)

The currently implemented design solves all these problems pretty elegantly. Cedric's original post was that make methods were just a convention, but if anything the proposed changes move away from formally marking methods as constructors towards more convention. I think the convention of methods starting with make as constructors or factories is actually really good. It is way better than what is done today in Java-land (which has a zillion incompatible conventions for factories, IoC, etc).

The advantages of the current design:

  • solves the inheritance problem - constructors are special, they aren't inherited, so each class can have its own make method
  • cleanly marked as constructors for documentation, reflection, IDEs
  • static on the outside like a factory
  • instance on the inside (convenient because you get the implicit this)
  • source level compatible between static factory and constructor (maybe we can make binary)
  • implicitly bound to allocation
  • have a lot of code and experience which prove they work

So I've come full circle - I haven't seen anything that really seems better than what we have today. I've accepted the fact that any solution is going to suck in some ways. And the current design seems to suck less than the alternatives.

I don't really think we can simplify the existing design much more (just move around the complexity). So I would put the question back out there - what problems are we really trying to solve with the current design?

There are two problems which I do think need solving, but I don't think they require changing the current design:

  1. simplifying the calling syntax
  2. solving the post-validation problem

Calling Syntax

I think everybody is in favor of allowing a shorter constructor syntax such as Type(args). We are already doing this for simple serialization by making Type("...") sugar for Type.fromStr("..."). So I propose we just enhance that feature a bit such that Type(args), the construction operator, is resolved as follows:

  1. if the arguments match Type.make then bind to it (whether constructor or factory)
  2. if the arguments match Type.fromStr then bind to it (existing behavior to keep serialization true subset of language)
  3. otherwise compiler error

The nice thing about this feature is we could add new rules in the future, so it decouples the caller from the actual implementation a little (at the source level only). Plus we are already using make in this way for Type {...} which is sugar for Type.make {...}.

Validation

The other problem is validation. My original proposal called for an invariant method - it seems that name was pretty universally disliked. Other suggestions were verify or check. Since then I've done a 180 and agree with Cedric it should be a facet. My proposal is that any method annotated with the @onNew facet is run after both the constructor and its associated with-block. These are called onNew handlers and must have no parameters. The onNew handlers are run in order of superclass down in declaration order. By convention they are called check:

@onNew private Void check() { }

My intention is that callbacks start with "on" by convention. I'm using that naming convention heavily in the FWT also.

Comments?

jodastephen Thu 17 Jul 2008

On calling syntax, I'm happy - I think most proposals seem to agree with this general approach.

On validation, I remain unconvinced that key language features should be implemented using facets. And construction is about the most fundamental feature there is. In addition, the interaction of subclass validation and superclass validation needs definition in your proposal.

The simplest adjustment to your proposal would be to change the signature from @onNew private Void check() {} to keyword {} where keyword is perhaps verify. It then ceases to be a normal method, but is effectively a constructor with the compiler calling the super-verify method.

However I still come back to the issue of setting const values in a construction-time with-block. IMHO, this is the heart of the problem, and the key problem that is being faced.

The issue I see in different threads is fundamentally "what is a with-block". Once construction is completed, a with-block is simply a means to call the public API. Find, simple and it works.

However, during construction this isn't the case. One view is that the construction-with-block simply calls the API after construction - however that view is flawed because const fields can be set. The other view is that the construction-with-block calls the API as part of the construction process - however that view is flawed as there is no way to control it.

The solutions are to make one of these view accurate - either ban const field setting or accept that they are part of construction not something separate from construction.

jodastephen Thu 17 Jul 2008

A question: does the construction-with-block assign data to fields directly, or call setters?

brian Thu 17 Jul 2008

The simplest adjustment to your proposal would be to change the signature from @onNew private Void check() {} to keyword {} where keyword is perhaps verify.

I am not opposed to that approach, but I like the facet design because it easily lets you create multiple initialization methods, and can be made to work with mixins also. I'm not really in favor of using something like verify or check as a keyword. I might be able to live with assert since Java has taken it already.

However, during construction this isn't the case. One view is that the construction-with-block simply calls the API after construction - however that view is flawed because const fields can be set.

I don't see why it is flawed - the compiler lets you set const fields when it knows the set to be safe. I don't see why whether this happens in a ctor proper or on the outside via a with-block makes much difference (validation not with standing). And it is pretty much what you have to do in deserialization. The whole feature is what gives Fan its declarative nature.

You make a fundamental distinction b/w normal with-blocks and construction time with-blocks. I think we are on different pages here. I see these as pretty much exactly the same, just that setting const fields has an extra compiler rule (which is all final really is in Java also). In the end it just a storage location, and we're trying to make a good effort to get the object setup before it gets passed around (potentially to other threads). Not perfect, but very practical.

jodastephen Thu 17 Jul 2008

I think I'd categorise my view as an expectation of what construction means.

In my world-view, I expect an object to be fully initialised wrt its fixed state (const) once constructed. With Fan, this isn't the case - the object might, or might not, still have further initialization after being constructed. I find that really quite icky.

Its kind of like an extended remote constructor - effectively allowing the application code that intantiates an object being able to place code inside the virtual constructor. That feels very anti-encapsulation.

My response is to find ways to make the construction-with-block part of the construction itself. I want all construction to be consistent, and ideally for all construction to be via a factory. I have a sense I'm getting closer to some of Gilad Bracha's thoughts ;-) Maybe I need to re-read that blog post.

Specifically on this proposal here's a "positive related thought". If you use a facet for @onNew, then you should also consider a facet for @onChange. @onChange Void check(Field[] fields) would be a method called whenever one, or more, fields are changed - one if it is a setter, more than one if it is a with-block. This would provide a simple means to apply invariant checking and cross-field validation. It would also avoid the need for coded setters in many cases as it would be simpler to write an @onChange.

tompalmer Thu 17 Jul 2008

I like Stephen's anonymous new {/*...*/} blocks more than the facet. I think that allowing more than one and being dependent on declaration order are both bad news.

And I think the keyword new isn't confusing if explained correctly. Just say, "You can also provide an anonymous new block that is run after the with block of any constructor call."

Or something like that.

JohnDG Thu 17 Jul 2008

I think that allowing more than one and being dependent on declaration order are both bad news.

Not to mention the name clashes that result from inheritance. Going to see stuff like, verify, verify2, verify3, verifySDFJOI.

Do like the @onChange idea (invoked after single setter or invoked after with block), though. Could be extremely useful.

brian Thu 17 Jul 2008

In my world-view, I expect an object to be fully initialised wrt its fixed state (const) once constructed. With Fan, this isn't the case - the object might, or might not, still have further initialization after being constructed. I find that really quite icky.

Isn't that just a matter of what you are calling "constructed". Coming from Java, you consider the constructor the only phase of construction. With-blocks add a second phase of construction to allow declarative programming and avoid writing ridiculous constructors. That second phase has required a third validation phase (syntax to be decided).

Java lets you put all three phases in one method, but sucks at declarative programming. I really want declarative programming so I'm willing split construction into three phases. But in the end it is still just "construction".

The focus of these discussions was really how to mix traditional constructors with declarative programming.

I've kind of lost track of your current position Stephen - are you arguing against keeping constructors as is? Or are you ok with that and arguing for how to do phase 3 validation?

If you use a facet for @onNew, then you should also consider a facet for @onChange.

I dig the @onChange facet, and was kind of planning on something like anyhow - letting the compiler generate boiler plate for me based on facets.

I like Stephen's anonymous new {/*...*/} blocks more than the facet.

Tom are you saying keep ctors like they are, but use a new {} block as the phase 3 validation? It is a little confusing to use new like that, but I think I could go that route.

jodastephen Fri 18 Jul 2008

Coming from Java, you consider the constructor the only phase of construction. With-blocks add a second phase of construction to allow declarative programming and avoid writing ridiculous constructors. That second phase has required a third validation phase (syntax to be decided).

As a Java developer, I understand factory methods, and I understand constructors. But with Fan, construction doesn't finish at the constructor, and that is a bit of a mental shift.

// when does construction end?
point := Point()            // after the Point()
point := Point() {x=10}     // after the Point()? not really sure
point := Point(5) {x=10}    // after the Point(5)? not really sure
point := Point {x=10}       // after the {x=10}
point := Point.make {x=10}  // after the {x=10}
point := Point.make         // after Point.make
  {x=10}                    // ...or is it after the {x=10} on a newline ?

Basically, you can't tell where construction ends without understanding the rules, whether the make factory is a constructor or not and knowing whether a with-block is following. One alternative would be to replace construction-with-blocks with named parameters:

Point(x=0, y=10)

Now, my Java sensibilities are all happy - at the end of this "constructor" the object is fully initialised. Totally obvious and clear - consts can only be set in the constructor. (Note that there still are three implementation phases - factory, allocate/assign and validate). Of course this could affect the serialisation syntax - or just mean serialization isn't a language subset.

However, no one else seems especially bothered by the issues I feel with construction-with-blocks. I know you like them as is, so unless someone chimes in, I'll probably have to leave this point.

I've kind of lost track of your current position Stephen

My position is still in favour of the idea I outlined in the other thread:

  • static factories mangle input
  • construction-with-blocks are calls to a hidden constructor and allocate and assign data (although I'd prefer constructor-named-parameters)
  • a validator validates The hard parts with what I proposed are superclasses (super=...) and the terminology for each individual concept.

I'm also arguing against using facets for key language features.

jodastephen Fri 18 Jul 2008

As I didn't know, I thought I'd try one of the examples above:

class Name {
  const Str name
  new make(Str name := null) {this.name=name;}
}
nm := Name("Bob") {name="Sue"}

So, what does nm equal?

  • compile error (once the constructor is complete, the const field cannot be changed)
  • Bob (the name field is const, so Sue is ignored)
  • Sue (the with-block is treated as part of the construction)

The answer appears to be the third option - "Sue". My expectation was a compile error.

The confusion arises, because it looks like you've called a constructor, and so all the const state should be fixed. But it isn't, as the with-block can override any const state setup in the constructor! Yuck ;-)

And in this example:

nm := Name("Bob")
{name="Sue"}

the same thing happens. Which is perhaps even worse, as a quick visual glance would definitely assume that nm has the value "Bob" because of the newline.

JohnDG Fri 18 Jul 2008

You're not the only one, Stephen. I also think the notion of construction is currently muddled (a strange mix of Java & Fan) and likely to confuse many people. But there are strong opinions on the other side of the fence and construction consists of such a small amount of code (versus total) that it's not worth spending too much time on.

The things I like most from Stephen's proposal is that it's so Fan-like and clearly separates the construction phase from everything else. Fan-like in that the keyword new (valid only in the scope of the class) essentially returns a chunk of blank memory onto which the subsequent with block performs initialization (so from this perspective, it makes perfect sense it is allowed to set const fields and the like). Moreover, it guarantees the verify block is invoked after construction (and in the future, deserialization) for both the class and any superclasses, and forces all client code to use atomic construction (no separate back-and-forth phases where client code participates in the construction of an object). Nice strong semantics, clear rules, and very unified behavior.

Named parameters in general would be extremely cool, but not so much for calling functions (except maybe constructors) as for reflection. Currently, after compilation, information on the names of parameters of methods and constructors is forever lost. Many times I've wanted that information when designing convention over configuration code that looks at signatures to determine what to do.

andy Fri 18 Jul 2008

My brain is a bit mush after all this - but if I gathered Brian's ammended proposal correctly, I am on board with that:

  • Single ctor (though would prefer this be called new)
  • With blocks continue to work as they do
  • New @onNew facet to call methods post-construction

Yeah, there is a gotcha there as Stephen has pointed out, but every language has its gotchas, and that seems a minor issue. Its unlikely you'll be using both a ctor and a a with block. But even so, I don't consider that a big problem. The gains in simplicity clearly outweigh that to me.

brian Fri 18 Jul 2008

Currently, after compilation, information on the names of parameters of methods and constructors is forever lost.

Actually it is reflective in Fan:

fansh> Str.type.method("replace").params
[sys::Str from, sys::Str to]
fansh> Str.type.method("replace").params[0].name
from

I think named parameters could solve the constructor issue but they don't let me use serialization as an expression.

Just to make sure everybody understands - the reason I'm so obsessed with with-blocks on constructors and being able to set const fields is because that is the only way to allow the serialization syntax to be a subset of the language. This feature is really, really important to me - if you've looked thru some of the FWT demo code you can probably appreciate it. A serialized Point looks like:

Point { x=10; y=20 }

I want that exact same syntax to work in code as an expression. Hence the with-block design. Since x and y can sometimes be const, I need to set them in the with-block.

I can't argue that this design is muddled and yuck.

JohnDG Fri 18 Jul 2008

I want that exact same syntax to work in code as an expression. Hence the with-block design. Since x and y can sometimes be const, I need to set them in the with-block.

Yes, given this requirement, it's clear what the design must look like.

In which case I suggest one way to remedy the bifurcated nature of construction is to disallow () style construction when with-block initialization is used (if that's even viable).

I had no idea Fan provided reflection on parameter names. Kudos! You can't imagine how many times I've wanted this in Java.

cbeust Fri 18 Jul 2008

I'm getting a bit confused as well with all the proposals flying...

I have to say that if the current proposal allows all the variations that Stephen posted in his "// when does construction end?" sample code, then I am really concerned.

If not, Brian, could you post a similar enumeration of all the correct and incorrect syntaxes along with what they do? At this point, it's the only way for me to decide whether what we have is intuitive or over-engineered.

As for the @onCheck idea, I'm concerned that it might impact the performances quite a bit, so I would only feel okay with it if it can be completely ignored by the compiler (ideally, by default).

Overall, I have to say that I find the ideas professed by Design by Contract largely useless, especially class invariants. Either these invariants are simple and they can easily be expressed by asserts, or they are complex and you are much better off capturing them in tests.

-- Cedric

tompalmer Fri 18 Jul 2008

I'm against @onChange at this time. I think general triggers support should be part of a larger scale coherent AOP strategy. And that might require more thinking. Let's just improve the call syntax for now (and maybe the new {/*...*/} thing, and yes Brian, you understood my recommendation) and deal with other fanciness later.

jodastephen Fri 18 Jul 2008

I want that exact same syntax to work in code as an expression. Hence the with-block design. Since x and y can sometimes be const, I need to set them in the with-block.

Given this as a requirement (and I do appreciate it), I come to the same conclusion as JohnDG - that you should not be able to use a construction-with-block together with constructor parameters.

Another example of the problem of not disallowing this (untested) would appear to be:

// valid today - name is set to "Sue"
Name("Bob") {name="Sue"}
// refactor - now it is invalid, as name is const
createBob() {name="Sue"}

In other words, because it looks like two expressions, you assume refactoring is safe, but its actually one compound expression.

While I would prefer named parameters, I could live with JohnDGs proposal:

Name("Bob")               // valid
Name {name="Sue"}         // valid, can set const
Name("Bob") {name="Sue"}  // invalid as name is const (this is a normal with-block now)
Name("Bob") {age="Sue"}   // valid as age is not const (this is a normal with-block now)

This makes it clear as to which case has the extra setting const power.

At the very least, the following current confusing syntax should not work:

nm := Name("Bob")
{name="Sue"}

Achieving that simply requires that any construction-with-block must start on the same line as the type name. Of course that clashes with where you guys tend to put braces.

Note that all this is about how the caller view the world of construction. Its orthogonal to the internal layout of factories/constructors/verifiers, where I still support my proposal, again summarised well by JohnDG.

JohnDG Fri 18 Jul 2008

One idea to unify Stephen's proposal with the serialization requirement: client code can itself invoke new blocks:

Foo.new {
   constField = "hello world"
}

in which case the client code is given the same expressive power as factory methods inside the class itself.

Meanwhile, the vast majority of client code will use standard methods:

Foo(12, "bye")

Foo.makeFromBar(bar)

Foo {
   num = 12
   text = "bye"
}

Haven't really thought about the implications of this but it seems like it could work and is consistent enough for easy parsing and implementation.

andy Fri 18 Jul 2008

Given this as a requirement (and I do appreciate it), I come to the same conclusion as JohnDG - that you should not be able to use a construction-with-block together with constructor parameters.

I could live with that.

JohnDG Fri 18 Jul 2008

Overall, I have to say that I find the ideas professed by Design by Contract largely useless, especially class invariants.

Depends on your coding style. If you do lots of imperative coding, using shared session data and many mutable structures, you might not benefit much from DbyC, but if you have a more functional style, you could well see benefit.

In any case, DbyC is a small part of what @onChange is about (so small it hardly merits discussion). @onChange is essentially compiler support for an internal-only implementation of the Observer design pattern, and as such, it shares many of the same applications as Observer.

For example, in a widget toolkit, you might want the view to update after changing a bunch of widgets in a container, and @onChange provides a simple and beautiful way to accomplish that.

Thinking about it more, I'm also starting to like @afterWith (which has some overlap with @onChange, but to what degree I am not sure). This feature would pave the way to really cool use cases:

db.createTransaction
{
   insert(newRecord)
   // ...
}

where the commit operation happens after the with block.

Straightforward AOP can't do this because only the compiler has a notion of what constitutes a with block. So you really need language-level support for this.

Although you might not implement it using facets -- it could just as well be a magical method like trap.

jodastephen Fri 18 Jul 2008

One idea to unify Stephen's proposal with the serialization requirement: client code can itself invoke new blocks

That is how I'm already thinking of it. If my proposal were adopted, then these two are equivalent:

Foo.new { constField = "hello world" }
Foo { constField = "hello world" }

JohnDG Fri 18 Jul 2008

That is how I'm already thinking of it. If my proposal were adopted, then these two are equivalent:

I'm not sure they should be equivalent as use of new carries with it additional obligations/privileges that are not ordinarily needed (super assignment being one of them), whereas Foo would merely construct using default constructor, presenting safer and dumbed-down semantics.

brian Fri 18 Jul 2008

Given this as a requirement (and I do appreciate it), I come to the same conclusion as JohnDG - that you should not be able to use a construction-with-block together with constructor parameters.

I actually do that quite often now, some code I just wrote:

InsetPane(10,0,10,0) { content = ... }
CallExpr.make(init, toImmutable) { isSafe = true }

The key point is that very often you want by order parameters passed to your constructor, but might have lots of other rarely used fields to set using a with-block.

While I would prefer named parameters

I would also prefer named parameters, but I don't know how to rectify that with serialized expressions. You would have to either use parens in serialization or else use {} for names parameters.

Achieving that simply requires that any construction-with-block must start on the same line

I think we can safely say any whitespace restrictions on curly braces is a show stopper :-) That is a religious battle unlike () and [] which I'd rather Fan avoid.

jodastephen Fri 18 Jul 2008

The key point is that very often you want by order parameters passed to your constructor, but might have lots of other rarely used fields to set using a with-block.

But thats fine, so long as the fields set in the with-block are public API fields, because this is just a chain of two expressions, a constructor and a normal-with-block:

CallExpr.make(init, toImmutable) { isSafe = true }

This is valid, so long as isSafe is not const or private.

I would also prefer named parameters, but I don't know how to rectify that with serialized expressions. You would have to either use parens in serialization or else use {} for names parameters.

Actually from my limited testing of Fan I reckon that deserialization-with-blocks are a third type of with-block in the current Fan syntax:

  • normal with-block - (acts on an object separate from construction) - can call the public API
  • construction-with-block - can call the public API and set consts
  • deserialization-with-block - can set fields including consts and privates

Each of the three has different abilities, related but different.

Brian, perhaps you could summarise in another thread why it is important for serialization to be a subset of the language. (I have some other ideas on serialization I'd then like to chip in)

I think we can safely say any whitespace restrictions on curly braces

There have been rules added recently to say that method parameters must start on the same line as the method name IIRC. This is just a variation on that theme.

alexlamsl Sat 19 Jul 2008

Hmm - is there too many rules and exceptions by now? :-/

brian Sat 19 Jul 2008

But thats fine, so long as the fields set in the with-block are public API fields, because this is just a chain of two expressions, a constructor and a normal-with-block:

The problem is that immutable classes are pretty common, and we want to encourage their use - not make it more difficult to work with them. So I'm not sure I agree with that statement.

deserialization-with-block - can set fields including consts and privates

I'm not sure about this (and I haven't really laid down the specification with regard to protection scope). Fan is quite unlike Java in that serialization is an extremely public thing. I've struggled with this problem in previous systems of my career.

jodastephen Sat 19 Jul 2008

Agreed that immutable classes are common. But let me just outline my statement again so we are clear:

// Point has const x and non-const y
Point(0, 10)          // valid, constructor sets x and y
Point {x=0; y=10}     // valid, construction-with-block sets x and y
Point(0) {y=10}       // valid, constructor sets const x, normal-with-block sets non-const y
Point(0) {x=0}        // invalid, const status was complete after constructor, so normal-with-block cannot set non-const x

This seems a relatively small change to the current semantics with a gain in clarity.

brian Sat 19 Jul 2008

This seems a relatively small change to the current semantics with a gain in clarity.

Right now my position is that a class's const fields are "open" until all phases of construction have run: constructor, with-block, and the validator.

I'm not in agreement that there is a gain in clarity, because it will force people to always pass const fields thru an explicit constructor, and I want to avoid things like Swing constructor hell.

jodastephen Sat 19 Jul 2008

it will force people to always pass const fields thru an explicit constructor

Thats not what I suggested. See the second one of the four cases.

brian Sat 19 Jul 2008

Thats not what I suggested. See the second one of the four cases.

If I understand you right you are saying that you can use either a constructor or a with-block, but not both at the same time. It is your last case which I want to allow.

jodastephen Sat 19 Jul 2008

I am saying that you can have both a constructor and a with-block, but you can't set const fields in the with-block if you do have both.

See my new thread for a new take on this.

alexlamsl Sun 20 Jul 2008

A question just come to mind - do we have plans to prevent validation method(s) to change state of the instance it is inspecting?

brian Mon 21 Jul 2008

Yes, I think no matter how we approach the three phase construction that all phases are allowed to set const fields. Once the construction is complete, then const fields are immutable.

alexlamsl Tue 22 Jul 2008

So even validation can change const fields?

tompalmer Tue 22 Jul 2008

I personally think any post-new-with block should be allowed to do so.

Login or Signup to reply.