#288 New-block/New-method proposal

jodastephen Wed 16 Jul 2008

A new thread from this one.

This proposal builds on the following principles:

  • construction with blocks are a Good Thing
  • construction with blocks must be validated (as they can set const fields, and are called from serialization which could be maliciously altered)
  • a simple solution is required
  • there are four phases
    • (1) conversion - where input parameters are adjusted to the storage fields
    • (2) allocate - where the memory is reserved
    • (3) assignment - where the data is assigned to the storage
    • (4) validation - where the data is checked (this could be before assignment)
  • superclasses must be handled

Proposal

This proposal uses the concept that factories best represent (1). Validation (4) must happen based on either the with-block data, or the data from the factory. Allocation (2) and assignment (3) are automatic.

Here is the example class, a line:

class Line {
  const Int startX := 0;
  const Int startY := 0;
  const Int endX;
  const Int endY;
  ...
}

Factories and the new block

Factory methods are declared using a This return type:

class Line {
  ...
  static This make(Point lineStart, Point lineEnd) {
    return new {
      startX = lineStart.x;
      startY = lineStart.y;
      endX = lineEnd.x;
      endY = lineEnd.y;
    }
  }
  static This make(Point lineEnd) {
    return new {
      endX = lineEnd.x;
      endY = lineEnd.y;
    }
  }
  static This make(Int endX, Int endY) {
    return new {
      new.endX = endX;
      new.endY = endY;
    }
  }
}

The new {} block only needs to define those fields that have not got a default value. However, defining a new {} block without a required field would be a compile error.

The new {} block can use the new. prefix to refer to the fields of the new instance as opposed to the local scope. This is similar in concept to the this. prefix.

Factory methods are basically ordinary methods. They can call other factory methods, and return cached instances, proxies or subclasses.

The new {} block can also be used directly on the public API, aka a with block:

line := Line {startX = 0; startY = 10; endX = 20; endX = 30; }
// syntax sugar for:
line := Line.new {startX = 0; startY = 10; endX = 20; endX = 30; }

The new validator

The new validator (4) is autogenerated by default:

class Line {
  ...
  new {
  }
}

However, it may be specified:

class Line {
  ...
  new {
    if (endX == null || endY == null) throw NewErr.new
  }
}

Allocation (2) and Assignment (3) take place automatically based on the named parameters passed to the new {} block. These stages occur however the class is instantiated (factory/with-block/deserialization). The code within the new validator can assume that the fields have been assigned.

Declaring the new validator serves two purposes. Firstly, it provides for validation (4). State can be checked - and this occurs after both normal construction, with-block construction and deserialization.

Secondly, the new validator can have access control added. Adding access control may affect deserialization (ie. it might prevent the class from being declared serializable).

Simple assignment factories

A final possible factory style (and this is an optional part of the whole proposal) is syntax sugar for declaring simple factories that just assign data:

class Line {
  ...
  static new make(startX, startY, endX, endY);
}
// syntax sugar for
class Line {
  ...
  static This make(Int startX, Int startY, Int endX, Int endY) {
    new {new.startX=startX; new.startY=startY; new.endX=endX; new.endY=endY}
  }
}
// calling style
line := Line(0, 10, 20, 30)
line := Line.make(0, 10, 20, 30)

Here, we are defining a factory taking the four listed fields in the order specified. Note that the types of the fields do not need to be declared as they are picked up from the field definitions. All fields without a default value must be listed. Any fields at the end of the list that have a default value would be optional.

Superclasses

Superclasses are handled as proposed by JohnDG, by assigning to super:

class ColouredLine : Line {
  Colour colour;
  // simple assignment factory
  static new make(startX, startY, endX, endY, colour);
  // normal factory
  static This make(Point start, Point end, Colour colour) {
    return new {super=Line(start,end); new.colour=colour}
  }
}

The new {} block call must contain either a super= or initialize all the fields of the superclass. If the subclass needs to initialize the superclass using any more complicated mechanism, then the subclass must write factory methods.

Abstract superclasses may have factory methods and use the return new {} syntax. Such a factory can only be called from a subclass new {} block.

The new validator of the subclass only needs to validate its own state. The new validator of the superclass will already have been called:

class ColouredLine : Line {
  ...
  new {
    if (colour == null) throw NewErr.new
  }
}

Summary

Essentially, this proposal only allows objects to be created by calling the named parameters with-block style new {} block. Factories and the new validator then perform the pre and post processing.

Finally, apologies for the long post - hopefully, this is clear enough to discuss...

tompalmer Wed 16 Jul 2008

I sort of like it, but I want my Point(x, y), and I'm really not sure the validator is needed. Also, how to make the default new nonpublic?

I also have yet another variation on Brian's latest recommendation on the other thread, but I'll defer that for now.

jodastephen Wed 16 Jul 2008

You can have Point(x,y) - simple declare a simple assignment factory. The calling style, ie. Type(args) is a separate issue to the underlying factories/constructors.

The easiest way to think of the new validator is as the single constructor - you just don't have to declare the arguments (as they are passed and assigned as named parameters automatically).

brian Wed 16 Jul 2008

Stephen - thanks for the detailed proposal. I'm still trying to digest it and figure out how it differs from the single constructor proposal.

I think the big differences are:

  • the new declaration is for the post-construction routine (new validator)
  • new {} block is the constructor (allocation)
  • compiler ensures that your super class new validator is always run
  • initialization computation is done in a static method which utilizes new {} block

What I like most is that you've flipped the initializer method and validator method, so the natural "new method" does post-construction validation. I'm not sure how intuitive that is, but it a good direction to think in because it avoids all the issues we've had about what to call/how to annotate the post construction method(s).

Although these are the problems I see:

  • I think my current single constructor proposal is easily grasped by a Java/C# programmer (more so than the currently implemented design). This requires a new way of thinking - what advantages does this approach have to make that difference worth it?
  • I'm having trouble understanding how a static method could have a This return since it will always be the declaring class - but I think you proposed that so I wouldn't have to type the classname? Maybe that might be a nice feature for all static methods, currently I flag as compiler error.
  • The new inside the new with-block isn't really needed since it implied
  • The new with-block isn't quite as convenient as just working in a normal instance or ctor method
  • The "JohnDG super proposal" seems elegant, but seems weird to me (back to the advantages over different question)
  • It seems a bit counter intuitive that new is called after construction
  • How does the compiler generate a "default constructor" for you?

tompalmer Wed 16 Jul 2008

So the new validator (and the fact that it's run after rather than before the with block) is really the novel feature of this proposal. And the style of super call. I think everything else is mostly doable with current other proposals.

I'm afraid that having some setters only for construction could be a bit odd. Maybe better to pass in other config objects if we want named parameters.

Do static factories really not play well with const initialization? If that's true (and it makes sense at the moment since factories could supply previously created or unrelated objects anyway), then I'm actually starting to favor the idea of not changing Fan's current model much at all. I just have a couple of alternatives for slight modifications that I think we should consider.

My alternatives for slight changes to current Fan (1.0.28) constructors:

  1. Let Point(x, y) call Point.make(x, y) (and maybe not support fromStr with this syntax).
  2. Since new make() looks weird, just let the default constructor be called new instead of make, and as a special case, don't require the redundant word (so just new() {/*...*/}). Again Point(x, y) calls Point.new(x, y).

And any other constructors can be called "supplemental constructors" (constructors just like today's fan, such as new fromStr(Str str) {/*...*/}. These are also called just like today Blah.fromStr("something")). Again, the point is to allow some custom logic while playing nice with const and with blocks.

The question raised by Stephen, I think, is whether it's better to have multiple constructor methods or to allow custom handling in one constructor after setters have been run.

I currently vote for just keep Fan as is today with one of the two slight modifications recommended above.

tompalmer Wed 16 Jul 2008

I posted before seeing Brian's reply, by the way. Apologies on any resulting confusion.

JohnDG Wed 16 Jul 2008

I really like this proposal for the following reasons:

  1. It completely eliminates constructors in the ordinary sense. Constructors are one of the most abused tools in all of object-oriented programming. I've seen constructors that span more than a hundred lines of code with many points of failure. Similarly, I've seen classes with 10 or more constructors, having several boolean parameters to turn various settings on and off, which are a royal pain to decipher.
  2. It puts object construction where it really belongs: in factories.
  3. It performs automatic validation for superclasses. This really is the right direction -- the semantics are harder to misuse.
  4. It provides a nice route to validation after deserialization.
  5. It permits a lot of flexibility in superclass initialization. Brian has noted that assigning super is weird -- however, super is already treated as a field/slot, even in Fan, because you can access methods using the notation super.. super can be viewed as just another slot, one that happens to point to the superclass, with "automagic" delegation to this slot for non-overridden methods. With this view, it's natural to want to assign super.

Now the one thing I don't like in this proposal is the conflicting uses of new and the non-standard with block. In one instance, new seems to create a new instance and the subsequent with block in the factory method initializes the new instance. However, in the other instance, new is used to perform validation after allocation and initialization. This is confusing.

In my view, new should refer only to allocation, so the syntax of the factory methods would remain unchanged, but the verify method would be changed to something like verify. e.g.:

class Line {
  ...
  verify {
    if (endX == null || endY == null) throw NewErr.new
  }
}

Even if this proposal isn't accepted, I agree with Brian that it's a productive way to think about the problems involved.

jodastephen Wed 16 Jul 2008

Brian, I think part of the problem is that I'm still a little unclear as to the exact details of how your proposal stands after the other long thread.

I also think that your summary of this proposal isn't quite right. I see the new validator as effectively being the "constructor". It implicitly takes all the fields as arguments for itself and its superclass using a named parameter style. They are passed directly from the new-block. As such, the only code you write in this "constructor" is the validation code.

The bytecode generated would be a real constructor with one parameter for each field in the superclass, followed by each field in the subclass. This would not be visible to Fan code (except maybe in reflection).

Effectively, the only way to instantiate an instance is using a new-block. A construction with-block (type (2)/(3) from the other thread) is simply syntax sugar for a call to the new-block. Although the new-block appears to be initialising the state, it is actually just passing named parameters to the "constructor".

On your points:

  • The This return type is a convenience which I was using to identify constructors. I'm not fussed about that detail.
  • The new. inside the new-block is needed to override the local scope in the same way that this. overrides the local scope to refer to the enclosing instance.
  • The super proposal actually opens up a more wild idea. (*What if Fan didn't have direct inheritance at all? What I mean is implementation using composition rather than inheritance. The calling code would see no difference, but within the class itself there would be a super field that holds a reference to an instance of the superclass - which could actually be a subclass of the superclass!*)
  • As I've explained, new validator is effectively the "constructor".
  • The default "constructor" is new {}. It still allows assignment to all fields including const, it just doesn't have any validation.

By the way, the use of Point(x,y) falling back to alternatives including make and new is something I think I can agree with.

Comparing

Comparing the proposals as best I can, I don't see how just having a single constructor solves the problem of validating with-blocks that are run immediately post-construction. If those with-blocks have special powers (ie. they can set const fields) then they must be vaildated. If they can only access the public API, then they are nothing special, and are perfectly safe.

Another factor to bear in mind is that IIUC then the JVM Memory Model requires that fields like const that are shared between threads must be declared final in bytecode. This means that with-block data must be set via a genuine JVM constructor.

So, the key advantage of this proposal is that however an object is created it is properly validated wrt with-block or factory:

Point(10,20)
Point.make(10,20)
Point {x=10;y=20}

All three eventually call the new validator "constructor".

The question is whether this is enough to justify the extra complexity. The problem is that as long as you require with-blocks to set const fields, you need the complexity.

jodastephen Wed 16 Jul 2008

Actually, having read JohnDG's verify idea, I quite like that. Same concept, better name.

tompalmer Wed 16 Jul 2008

Brian, how tough would it be to implement a post-with/new block feature? I'm not convinced it would really be used in practice, but if it were added experimentally (if not too much effort), then it would be a chance for people to see if they would really use it or not.

If it did exist, and if used, I think it would be used for more than validation, so I'm starting to think that the syntax or name should imply that.

brian Wed 16 Jul 2008

Brian, I think part of the problem is that I'm still a little unclear as to the exact details of how your proposal stands after the other long thread.

Tonight I'll rewrite up my proposal into a single coherent post.

I also think that your summary of this proposal isn't quite right. I see the new validator as effectively being the "constructor".

I'm having trouble wrapping my head around this, because I'm trying to map it how I actually emit a Java or IL constructor, and use helper methods. Some of it is terminology. I think what you are saying is:

  1. any initialization computation happens in the static factory method, which then:
  2. the with-new-block passes field values to allocatation/Java ctor which inits fields
  3. normal with-block runs and potentially sets fields
  4. new-validator runs to verify the state

My proposal is that 1 and 2 run in a normal Java style constructor and that 4 happens in some other method(s) annotated with the @onNew facet. I think both ways let you do the same things, but I think my proposal is a little more in-line with how Java/C# work today.

What if Fan didn't have direct inheritance at all? What I mean is implementation using composition rather than inheritance

Actually I'm a fan of prototype based inheritance (like Self), but with Fan I was trying to leverage the JVM/CLR OO model as much as possible.

Another factor to bear in mind is that IIUC then the JVM Memory Model requires that fields like const that are shared between threads must be declared final in bytecode

Actually neither proposal solves this because you can't ever really have generic with-blocks run after the constructor or during deserialization on final fields. In practice I don't think it will ever be a problem due to how objects move between threads. But if it was a problem we'd have to use volatile or synchronized to solve it.

Brian, how tough would it be to implement a post-with/new block feature?

Well the whole constructor rework is a huge amount of work, so I want to get it right before I start. The post validation step itself is a rather smaller piece of the whole puzzle.

If I were to write your code using my proposal it would look something like this:

class Line {
  ...
  static This make(Point lineStart, Point lineEnd) {
    // you make assumption this is a special construct, to me it just
    // means call the no-arg new method/ctor and apply the with-block;
    // just dummy of course because one of these would be the "primary
    // ctor" we'd force the subclass to use
    return new { 
      startX = lineStart.x;
      startY = lineStart.y;
      endX = lineEnd.x;
      endY = lineEnd.y;
    }

   // remember can't overload by params, this one needs a new name
   static This makeEnd(Int endX, Int endY) {
    // don't need special new syntax, because normal with-block works
    return new {
      endX = endX;
      endY = endY;
    }

   // here we actually have a normal constructor with/without parameters
   new (Point s := null, Point e := null)
   {
     startX = s?.x
     startY = s?.y
     endX   = e?.x
     endY   = e?.y      
   }

  // validation is done via any method tagged with @onNew facet
  @onNew Void verify() {
    if (endX == null || endY == null) throw NewErr.new
  }
}

class ColouredLine : Line {
  Colour colour;  // you must be British :)

  // subclasses have to route to single superclass ctor (just like 
  // now, except there only one)
  new (Point s := null, Point e := null) : super(s, e) {}

  static This make(Point start, Point end, Colour colour) {
    return new(start, end) { colour = colour }
  }

}

JohnDG Wed 16 Jul 2008

I think both ways let you do the same things, but I think my proposal is a little more in-line with how Java/C# work today.

That's true, but it looks out of place in Fan. Not as clean as Joda's proposal, nor as clean as the rest of Fan, generally.

You know you brought all this on yourself by introducing with blocks, don't you? :-)

jodastephen Wed 16 Jul 2008

1. any initialization computation happens in the static factory method, which then: 2. the with-new-block passes field values to allocatation/Java ctor which inits fields 3. normal with-block runs and potentially sets fields 4. new-validator runs to verify the state

I am proposing that (1) is always emitted as a JVM static method, and (3)/(4) are a JVM constructor:

// Fan
class Line {
  const Int startX;
  const Int startY;
  Int endX;
  Int endY

  static This make(Point lineStart, Point lineEnd) {
    return new { 
      endX = lineEnd.x;  // note odd order - end followed by start
      endY = lineEnd.y;
      startX = lineStart.x;
      startY = lineStart.y;
    }
  }
  new {  // or verify
    if (endX == null || endY == null) throw NewErr.new
  }
}
line1 := Line(point1, point2)
line2 := Line {startX = 0; startY = 0; endX = 10; endY = 20}

// Java equivalent
public class Point {
  final Integer startX;
  final Integer startY;
  Integer endX;
  Integer endY

  public static Line make(Point lineStart, Point lineEnd) {
    // odd order of definition in fan code is reordered to correct order here
    return new Line(lineStart.x(), lineStart.y(), lineEnd.x(), lineEnd.y());
  }
  public Point(Integer startX, Integer startY, Integer endX, Integer endY) {
    super();
    this.startX = startX;
    this.startY = startY;
    this.endX = endX;
    this.endY = endY;
    if (endX == null || endY == null) throw new NewErr();
  }
}
Line line1 = Line.make(point1, point2);
Line line2 = new Line(0, 0, 10, 20);

As shown, the JVM constructor simply takes each field in order of declaration (superclass fields would be listed before subclass fields). This means that the constructor is genuine, and can set final fields correctly.

...you can't ever really have generic with-blocks run after the constructor or during deserialization on final fields.

The new-block is not exactly the same language feature as a with-block. It has different abilities (setting const and followed by verification). You can only save the "generic with-block" concept if you lose the ability to set const fields (because then you don't need verification either).

// new-block
new {
  // operations on a newly created instance
  // may set const fields
}
// with-block
object {
  // operations on an existing instance
  // may not set const fields
}

You could think of it as "if the with-block is preceeded by the new keyword then it is a new-block and is used to initialise an object rather than change its state". Of course sometimes the new keyword is invisible:

line2 := Line {startX = 0; startY = 0; endX = 10; endY = 20}
// is syntax sugar for
line2 := Line.new {startX = 0; startY = 0; endX = 10; endY = 20}

tompalmer Thu 17 Jul 2008

I still think "change nothing" (except for call syntax) should still be an option on the table.

I plan to refrain from mentioning it again, though, as long as my opinion doesn't change. Hopefully I have the willpower to avoid posting too much.

brian Thu 17 Jul 2008

As shown, the JVM constructor simply takes each field in order of declaration (superclass fields would be listed before subclass fields). This means that the constructor is genuine, and can set final fields correctly.

This is very interesting, although I'm not sure how practical it would be. It would likely create some extremely large constructor signatures and very deep call stacks. Plus it doesn't help much with deserialization - I guess I could buffer everything up, then create one big ctor call, but then you couldn't deserialize off the stream as well.

still think "change nothing" (except for call syntax) should still be an option on the table.

I think I'm coming back around full circle on this - I'll create another post.

brian Fri 18 Jul 2008

I've gone back and reread this proposal carefully. As far saying that a class has a single new operator which allocates the instance - I really like that (and has been championed by Tom and Andy in the 2nd thread). The hard part is coming up with a design for initialization which chains well through subclasses and is a little more than "just convention".

This proposal uses this:

static This make(Point lineStart, Point lineEnd) 
{
  return new 
  {
    startX = lineStart.x
    startY = lineStart.y
    endX = lineEnd.x
    endY = lineEnd.y
  }
}

static This make(Point start, Point end, Colour colour) 
{
  return new {super=Line(start,end); new.colour=colour}
}

Current design uses this:

new make(Point lineStart, Point lineEnd) 
{
  startX = lineStart.x
  startY = lineStart.y
  endX = lineEnd.x
  endY = lineEnd.y
}

new make(Point start, Point end, Colour colour) : super(start, end)
{
  this.colour=colour
}

The current design uses less syntax and seems more obvious to a C++/C# programmer (and hopefully a Java programmer). So I'm not understanding how this proposal is more elegant that what we currently have. Am I just missing something? Please educate me :^)

JohnDG Fri 18 Jul 2008

Java supports infinite constructors and has no notion of with blocks. Fan, in its current form, supports one constructor and constructing with blocks.

Here's the key point: the purposes and semantics of constructors and constructing with blocks overlap.

This is why I've referred to the design as muddled, because as Stephen has pointed out, construction in Fan is not an atomic operation where everything takes place in a single privileged area (as it is with Java). Rather, it's potentially a three phase system, a back and forth between user and class code, where the implications of some constructs are neither obvious nor intuitive because of modal semantics (identical looking with blocks meaning two different things in different contexts).

Now much Fan source code does not even use constructors: it uses with blocks exclusively. So it's natural to view with blocks as a successor to constructors. That's the direction Fan is facing. Only instead of taking a pure approach, and trying to do all construction using with blocks, Fan's still trying to hold onto something that it's all but replaced for many (most?) classes: constructors, whose form is largely unchanged from C++, the ancestor of Java and C#.

Why is this bad? It's confusing. The semantics are modal. It requires developers to master the new while still holding onto the old. It leads to a bifurcation in classes. It makes the language more complex because there are more rules and more productions in the grammar.

So how about Stephen's proposal? Well, although not directly stated by Stephen, it's possible to view (and explain to new developers) that new is a function or operator that returns an uninitialized chunk of memory -- a blank slate, if you will (Is that correct? No, but it's close enough). Now new is a static function for every class, so that it can be accessed directly by static methods (as in the above examples) or by user code by prefixing the class name (e.g. Foo.new).

Because the slate is blank, you can do special things in the with block that follows: in particular, you can assign const fields and choose the super class by assigning super (but only if you need to; you could just as soon set the super fields directly).

After the constructing with block ends, the slate is no longer blank and it's too late to do these things any more -- it's a hard barrier you can't get through.

This is a beautifully clear mental model that enables developers to really wrap their minds around the semantics.

In particular, it should be obvious what the following code constructs do:

class Foo 
{
   verify 
   {
      // Do post-construction-with-block verification here
   }

   static This make() 
   {
      return new 
      {
         // Privileged operation on 'blank slate'
      }
   }
}

Foo.new
{
   // Privileged operation on 'blank slate'
}

Foo 
{
   // NOT privileged, just shortcut for make()
}

Foo(1, 2, 3, 4) 
{
   // NOT privileged, just shortcut for make()
}

In other words, new followed by a with block is privileged construction on a blank slate, while anything else is just a normal with block with no special privileges. So simple it can be described in 1 sentence with no exceptions.

Meanwhile, the verify block is executed after a constructing with block. Every single time -- without exception. It's also executed for superclasses. You can do all verification for the whole hierarchy in one place, all because in the constructing with block, you can do any necessary superclass initialization as well.

Although that's really all that's necessary, it might be convenient to provide a default make() method (if none is supplied by the user), which just consists of the uninitialized fields in the class (and its supers), in their declaration order. This would nicely handle classes like Rect and Point and many other data classes and eliminate what is sheer boilerplate in other languages.

For all the other stuff, you use static factory methods, which is really what you should have been using all along.

Now I should point out even though I've written a lengthy reply on this topic, and I do think Stephen's proposal makes Fan more consistent with itself (bad pun), construction does consist of a small amount of total code. So this is by no means all that significant an issue. Would it be nice to ditch constructors in favor of construction with blocks? Sure, for the numerous reasons listed above. Is it a showstopper? By no means. Fan has plenty of other nice features to keep me happy.

jodastephen Sat 19 Jul 2008

Good summary by JohnDG.

Here is the CallExpr example:

// Fan today
class CallExpr : NameExpr {
  new make(Location location, Expr target := null, Str name := null, ExprId id := ExprId.call)
      : super(location, id, target, name) {
    args = Expr[,]
    isDynamic = false
    isSafe = false
    isCtorChain = false
  }

  new makeWithMethod(Location location, Expr target, CMethod method, Expr[] args := null)
      : this.make(location, target, method.name, ExprId.call) {
    this.method = method

    if (args != null)
      this.args = args
    if (method.isCtor)
      ctype = method.parent
    else
      ctype = method.returnType
  }
}

// proposal
class CallExpr : NameExpr {
  static This make(Location location, Expr target := null, Str name := null, ExprId id := ExprId.call) {
    return new {
      super = NameExpr(location, id, target, name)
      isDynamic = false
      isSafe = false
      isCtorChain = false
    }
  }

  static This makeWithMethod(Location location, Expr target, CMethod method, Expr[] args := null) {
    return make(locaton, target, method.name, ExprId.call) {
       // this is a normal-with-block
      method = method
      args = args
      ctype = (method.isCtor ? method.parent : method.returnType)
    }
  }
  verify {
    if (args == null) args = Expr[,]
  }
}

For me, a lot now hangs on the discussion wrt consts, with-blocks and serialization on other threads. The more I dig into the three kinds of with-blocks, the more this proposal starts to feel a little like it may be tackling the wrong problem.

alexlamsl Sat 19 Jul 2008

One concern that I would have with the current proposal is the enforcement of initialisation contract by superclass.

Java:

public abstract class A {
    protected A(String a) {
        // do something with parameter
    }
}

public B extends A {
    public B() {}        // this fails

    public B() {
        super("");       // this works
    }

    public B(String a) {
        super(a);        // this works
    }
}

One way to get around the issue would be for new to be internal, however that would prevent us from simple cases of Foo.new. To balance these 2 concerns, I would suggest the ability for classes to define the visibility of their new, default being public

alexlamsl Sat 19 Jul 2008

Looking at what brian has written up as a side-by-side comparison, I have to admit that super = Line(...) looks worse (read: counter-intuitive) than simpler superclass constructor call.

I think initialisation should be done through instance methods rather than static methods - they should share an equal status alongside with-blocks that would also satisfy the proposed validation mechanism.

class Line {
  Point start, end;

  virtual verify() {
    if (start == null || end == null) throw Err.new;
  }

  once Void setPoints(Point start, Point end) {
    this.start = start;
    this.end = end;
  }
}

class Stroke : Line {
  Color color;

  override verify() {
    super.verify();
    if (color == null) throw Err.new;
  }
}

Line.new               // Error

Line.new {
  setPoints(a, b);     // OK
}

Line.new {
  start = end = c;     // OK
}

Stroke.new             // Error

Stroke.new {
  color = Color.RED;   // Error
}

Stroke.new {
  setPoints(d, e);
  color = Color.BLUE;  // OK
}

Stroke.new {
  start = d;
  end = e;
  color = Color.BLUE;  // OK
}

alexlamsl Sat 19 Jul 2008

Continue from that train of thought - I am thinking about readonly, and how its compile-time checks could be altered slightly to preserve the convenience factor.

class Obj {
  virtual Bool valid() {
    return true;
  }

  virtual Void verify() {
    if (!valid()) throw Err.new;
  }
}

class Line {
  readonly Point start, end;

  override Bool valid() {
    return start != null && end != null;
  }

  // Proposed: allow the following to compile
  once This initPoints(Point start, Point end) {
    this.start = start;
    this.end = end;
    return this;
  }
}

Line.new            // Not valid()

Line.new {
  initPoints(a, b); // OK
}

i := Line.new {     // OK
  start = c;
  end = d;
}

i.initPoints(e, f); // Compile-time error: start is read-only

brian Sat 19 Jul 2008

Here's the key point: the purposes and semantics of constructors and constructing with blocks overlap.

That is true, we are providing two mechanisms to initialize fields (which requires a third mechanism for validation). But we're in this boat because I strongly believe that both mechanisms are extremely valuable.

identical looking with blocks meaning two different things in different contexts

This is Stephen's point, and I'm not in agreement. If you believe this then you are saying that the = operator means two different things based upon use in a constructor or elsewhere. It means the same thing: assignment. Just there is a rule in the compiler to prevent reassignment (either with = or with-block) once fully constructed.

Only instead of taking a pure approach, and trying to do all construction using with blocks, Fan's still trying to hold onto something that it's all but replaced for many (most?) classes: constructors

During this process I've convinced myself that no matter what you must have some sort of help initialization methods. You can call them constructors, factory methods, initialization instance methods, etc. I think we are all in agreement on this, we still need the helper methods. The core of this design is what those "helper methods" look like and how easy are they to use.

My epiphany was that constructors remain the best technique for initialization helpers. They look like factories/statics on the outside, but look like instance methods on the inside. Their key advantage is that they work equally well for both clients and subclasses.

This is a beautifully clear mental model that enables developers to really wrap their minds around the semantics.

I am mostly in agreement with this. But I'm not sure the clear mental model works well in practice when it comes to writing real code:

This proposal seems will typically require more code than using constructors as they are today. You will pretty much always have an extra level of nesting via the new-with-block.

Writing the static helpers is more complex because you can't put statements into a new-with-block. So you have to pre-compute everything to local variables, so that you can init with one big with-new-block. Conversely initializing via a true constructor is easier to work with because it is an instance method on the inside with an implcit this.

If you are required to do Foo.new {} in order to set const fields, then you pretty much will always have to use that construct which doesn't seem right. For example in the FWT I define fields which have to be fixed by the SWT constructor as const:

// today
Text { multiLine=true }

// with this proposal I always have to write:
Text.new { multiLine=true }

And of course the key issue - this proposal doesn't work as well with subclasses. I'm still not exactly sure I use my super class's helper methods. The beauty of true constructors is that they naturally work for both clients and subclasses without worrying about the allocation problem.

Needless to say I haven't been convinced (yet) that this proposal actually makes my life as a developer easier than the current design.

jodastephen Sat 19 Jul 2008

constructors remain the best technique for initialization helpers. They look like factories/statics on the outside, but look like instance methods on the inside

I think I am moving towards this. The problem is that the factories can't write normal code inside the new-block, hence there will be lots of local assigns and so on.

The proposal above undoubtedly leads to more code for the developer to write. More code is a Bad Thing, unless its for a Good Reason.

The exception is the short factory concept, which I think current Fan might be able to adopt as a short constructor:

new make(startX, startY, endX, endY) {
  // each of these has been assigned to the field of the same name
  // by the time processing starts here
}

I think this, or a close variation could save on a lot of boilerplate.

In general, I'd still like a single constructor if we can however, with multiple factories around it. At present however, I can't argue enough of a case to say this proposal beats current Fan with a new{} validation block added.

However, I will start a new thread for my latest random idea (idea not proposal).

katox Wed 3 Sep 2008

This topic still stirs my mind. I've been tackling Wicket lately and though it is a sweet framework sometimes it is rather tricky to customize its default behaviour, specially if the developers didn't thought of that possiblity beforehand (fortunately they mostly do).

Going through sources I found out that not only wicket users are encouraged to put markup initialization to constructors but also that the same technique is used in most base classes like Application, Page, PageView, FeedbackPanel.

It is probably the first java application that I have seen with such a heavy constructor code.

It seems that they chose this way to avoid error-prone mandatory calls of some super.init() elsewhere (including overrides). They use quite a lot of methods while constructing objects including virtual methods which can be particulary tricky as the subclass hasn't start initializing yet (thus even directly assigned fields are still null). Because users can't override constructors due to the mandatory super() call there is a bunch of various factory methods to let them put their own classes in parent objects (encouraging them to subclass those factories). Lastly, wicket developers use lots of lazy binding through so-called Models which initialize themselves only after some other object really demands the data.

What do you think of this approach?

brian Thu 4 Sep 2008

I prefer the more declarative model via with-blocks. But anything you can do in Java with constructors, can be done in Fan today - so we aren't limiting your design choices. Fan brings some new options to the table via with-blocks and immutability (although we still don't quite have it all right yet).

katox Thu 4 Sep 2008

Fan brings some new options to the table via with-blocks and immutability

Exactly. I'm wondering if we could hammer those to a shape that we wouldn't need complex constructors (like in Java, we'd probably need some).

The biggest issue I have with the Wicket approach is that you write code and your objects are incomplete. So you do OO with crippled objects. You have to know precisely internals of classes which you extend and thus you are mostly tied up to a particular package version. Quite a few ropes to hang on.

But maybe you have to deal with complex constructors if you want to use inheritance... maybe.

brian Thu 4 Sep 2008

The fundamental problem is that anything complex needs to let the ctor client perform some setup - which with-blocks are a great way of doing. However after that client setup, the class needs to verify that things were setup correctly - this piece is still missing, and has been the topic of two months of discussion.

Who knew constructors could be so insanely hard to get right.

Login or Signup to reply.