#452 Construction via Builders?

jodastephen Tue 10 Feb 2009

Its late here, and I've had a strange idea. I haven't spent much time on it, but I thought I'd throw it out in case it stimulates another idea...

What is construction-with-blocks are really a special kind of builder? By this, I mean the Java builder pattern that is used to create complex objects.

Basic idea, is to compile as follows:

// user written Fan code:
r := Rect(10, 20) {top=30, right=40}

// Fan translates this to Java:
Rect r = new Rect.Builder(10, 20)
           .top(30)
           .right(40)
           .build();

The key idea is that c-w-b assigns are translated to methods on the builder, where the builder has the same set of fields as the main class it is building for.

The first builder method takes the normal parameters that would be passed to the constructor. The subsequent methods take the c-w-b data. This means that everything gets assigned into the builder in the right order.

When build() is called, this calls a normal constructor on the Rect class:

// translated to Java
public Rect(Rect.Builder builder) {
  // verify to data in the builder
  super(builder);
  // object is now safely published
}

(maybe the verify should be done in the build method of the builder?)

The idea is that once everything is verified, the top superclass, Obj, calls builder.assign(). This method causes the builder to assign its data field by field into the Rect object itself. This allows the class to then be locked down at that point.

This is really just the outline of an idea. It still has holes I'm sure.

I see it as interesting because it does what we tried and failed to do before, capture the c-w-b and pass it as a parameter to the constructor chain. Closures didn't really work for that task because they involve generating a new bytecode class for every call-site. Here, we only need one class per class that can have a c-w-b (the builder class), and that class captures the applications needs. It will handle the concept of adding children directly to the parent too.

Update: Just to be clear - I'm specifically not proposing anything specific about the syntax a user would write for Fan constructor/factory. This idea is about how it gets translated to bytecode using a hidden code generated builder.

brian Tue 10 Feb 2009

Actually I think you might be onto something really interesting there - maybe constructors really return a synthetic class that lets you do further setup, before locking the instance down. That might also be a way to handle immutable sets. I'll give it some thought.

Just thinking out loud, to restate your example:

pt := Point { x = 5; y = 8 }  

Really looks like this under the covers:

pt := Point.makeBuilder { x = 5; y = 8 }.build

class Point
{
  PointBuilder makeBuilder() { return PointBuilder() }
  private new make(Int x, Int y) { this.x = x; this.y = y }
  const Int x 
  const Int y
}

@synthetic class PointBuilder
{
  Point build() { return Point(x, y) }
  Int x
  Int y
}

That is kind of starting backward from implementation towards syntax - but I think we might be able to mold this into a really good general purpose construction feature.

alexlamsl Tue 10 Feb 2009

We can generalise this a bit:

foo := Foo { bar = calc; doStuff }

becomes:

class Foo {
  Bar bar
}

@sythetic class FooBuilder
{
  const |Foo| withBlock;
  new FooBuilder(|Foo| withBlock) { this.withBlock = withBlock }
  Foo build() {
    Foo result := Foo
    withBlock(result)
    return result
  }
}

foo := FooBuilder(|Foo f| { f.bar = calc; f.doStuff }).build()

I remember reading the Puzzler about various thread-safety issues with Java constructors; and with this particular definition of construction in Fan may be we can do away with such hazards...

jodastephen Tue 10 Feb 2009

That might also be a way to handle immutable sets.

Actually, I think it would handle with-blocks too.

Thinking out loud... We ideally want a way to validate the set of changes as a whole before accepting them or rejecting them (atomic commit of a set of changes to an object). This validation could be done against the builder fields:

Void with(FooBuilder b) {
  if (b.bar == ...) throw ArgErr()
  b.assign()
}

Again, the syntax could probably be neatened, and the assign hidden. Effectively it is commit and rollback, commit if the validation method completes normally, rollback (ie. garbage collect the builder) if an exception is thrown.

Your sample code is pretty much what I had in mind, although I'd prefer to see the data set onto the builder using chaining:

pt := Point.makeBuilder().x(5).y(8).build

as this removes any reliance on with-blocks (allowing them to use builders at a later stage.

I disagree with alexlamsl's closure version because it is too heavyweight in bytecode generated.

brian Wed 11 Feb 2009

I've given this some more thought, and not sure I still have a clear picture. I'm kind of thinking of along two lines:

  1. somehow a method can return a "builder" which is used to capture state and passed back to a method on the original object
  2. make with-block first class entities

One of the things I keep coming back to is that functional programming works because functions are first class entities. Does that mean we can do a better job with declarative programming using first class with-blocks?

The different between first class builders versus with-blocks is a bit fuzzy. Suppose a with-block is an anonymous class? Then we can pass them around kind of like tuples or records which can applied to other instances via some kind of structural typing. For example:

pt := {x=4; y=6}

Would effectively be creating a synthetic class:

TupleX
{
  Int x
  Int y
}

Now we have a way to pass with-blocks around, use them as named parameters, fixed maps, things to modify an immutable instance, etc. Then something like this:

Point { x=4; y=6 }

gets translated into something that "applies" the given anonymous class to a Point instance. I haven't gotten far enough in my thinking to have a clue what that really means.

jodastephen Wed 11 Feb 2009

Three approaches - builders, closures and your first-class-with-blocks.

Builders are simple, well defined and produce little excess bytecode (max one class per class, and one extra object creation when using a construction-with-block). They are also close to what a user would write by hand in Java (hence follow Fan's evolutionary principle.

Closures generate a lot of bytecode - one class for every call-site. I just don't think that amount of bytecode is sustainable.

First-class with-blocks require extra classes, but at a level linked to call-site types. The problem is how to get the data out of the TupleX and into the Point. This would seem to need reflection, so there is non real benefit over just using a map of string to value.

Ask the question - "what is a with block". If it is a way to apply a block of code (which could be from anywhere) to a class then use closures. If it is a way to apply a block of data (which could be from anywhere) to a class then your first-class with blocks makes sense. If it is a way to initialise a specific class from a specific call-site then builder makes most sense.

I'd also point out that Point {x=6; y=7} and Point {y=7; x=6} are distinct and different, as there could be a setter triggered that alters x when y is set and vice versa. This makes the first-class with-block more difficult to implement, if possible at all.

I still prefer builders, especially as they are syntax sugar from Java code.

brian Thu 12 Feb 2009

This would seem to need reflection, so there is non real benefit over just using a map of string to value.

No my basic (unfinished) idea is that a first class with-block is a block of data. When "applied" to a given instance it would use structural typing with compile time checking and efficient code generation.

Ignore the syntax, but let's consider this code:

data := { x = 5; y = 7 }
pt = pt.apply(data)

Would generate something like this under the covers:

class TupleXXX { Int x; Int y; make(Int x, Int y)... }

data := TupleXXX(x, y)
pt.x = data.x
pt.y = data.y

So we would use structural typing to see if the types in the tuple could be "applied" to the instance, then you would either get a compile time type error, otherwise an efficient assignment.

That is just a general idea, that raises all sorts of other questions. For example what does this mean:

block := { doA(); doB() }
x.apply(block)  

// compiles into
x.doA()
x.doB()

Data isn't actually a class unto itself, it just passing around a set of data and in this case method calls I want to invoke. Then apply would work similiar, assuming the target instance has a methods called doA() and doB() it would invoke it. That is a little mind bending, so not sure about that. In that case the first class with-block is packaging up both data and methods to apply to instances.

Then there is the question of how I type such a beast. Because if I want to manage the apply myself, then I need to declare a method to handle it. That is how I get my hooks to do constructor validation and immutable setters.

I kind of thing that is the right track, but the idea needs a lot more thought.

jodastephen Thu 12 Feb 2009

Unfortunately, you haven't solved the problem of ordering, where setting x before y is different from y before x.

The more I think about construction-with-blocks, the more they remind me of Josh Bloch's builder pattern.

// Java
Widget y = new Widget.Builder("2", 2.0)
   .model("2")
   .manufacturer("222")
   .serialNumber("12345").build();

// Fan
y := Widget("2", 2.0) {
   model: "2"
   manufacturer: "222"
   serialNumber: "12345"
}

brian Thu 12 Feb 2009

Unfortunately, you haven't solved the problem of ordering, where setting x before y is different from y before x.

Why would it matter? It wouldn't be any different than how with-blocks work today - the order of fields sets is sequential (then assumingly you have the validation checks afterwards).

The more I think about construction-with-blocks, the more they remind me of Josh Bloch's builder pattern.

Do you have any ideas of what the syntax should look like for explictly defining or applying the builder?

As a use case, what might it look like in your example above to validate the const fields model, manufactor, and serialNumber?

JohnDG Thu 12 Feb 2009

No my basic (unfinished) idea is that a first class with-block is a block of data.

I like this idea more than builders (though no complete builder proposal has emerged, so I might change my mind). A more powerful variant of this idea is used to great effect in JavaScript (though with more syntax, as this must always be used to reference members).

In this approach, Obj would get a new method called apply, which would accept a Block-type object containing method invocations or assignments.

A subclass wanting to validate fields set in a Block would override apply, call super.apply, and then do verification, throwing an exception if something went wrong.

class Foo {
    override Void apply(Block block) {
        super.apply(block)

        if (...) throw new InvalidArgError()
    }
}

Now the problem is that for a user of Fan, it's not clear where to put the verification code. Do you put it in the constructor, or do you put it into apply()? If you put it only in apply, then the user might not use a with-block to initialize any other fields, so your verification logic would never be invoked. It seems like you'd have to put some verification code in the constructor (for any parameters passed into the constructor), and more still in apply (for anything initialized there). Doesn't seem very elegant.

Yet the whole notion of structurally applying a block of assignments/invocations to an object is very attractive. More still, applying a block of code to an object, ala JavaScript. It allows you to factor out some kinds of duplication very easily.

Another thing to keep in mind is how this feature plays for immutable objects. Perhaps apply would return an instance, which would be this for mutable objects or a copy for immutable objects. This would also allow you to do things like instance replacements and flyweight objects (but only if the user used a with-block following construction).

class Foo {
    override Obj apply(Block block) {
        Obj replacement = super.apply(block)

        if (...) throw new InvalidArgError()

        return replacement
    }
}

jodastephen Sat 14 Feb 2009

what might it look like in your example above to validate the const fields model, manufactor, and serialNumber?

There are various possible Fan syntaxes, but they are all complex. This is due to the multi-stage creation process, not due to builders.

y := Widget("2", "ABC") {
  model: "2"
  manufacturer: "222"
  serialNumber: "12345"
}

class Base {
  const Str model

  static new make(Str? model:= null) {
    new {
      new.model = model
    } verify {
      if (new.model.length < 2) throw ArgErr()
    }
    safePublish(new)
  }
}
class Widget : Base {
  const Str manufacturer
  Str serialNumber

  static new make(Str? model:= null, Str? manufacturer:= null) {
    model = model?.toUpper()
    new : super(model) {
      new.manufacturer = manufacturer
    } verify {
      if (new.manufacturer.length < 2) throw ArgErr()
    }
    safePublish(new)
  }
}

(Again, this all collapses down normally, when there is no verify that block is omitted, and the new block can merge into the method to become a shorthand)

This keeps four basic steps

  • manipulate data before calling super()
  • assign data from args to fields
  • process with-block
  • verify state OK to create object
  • create and lock state
  • safe-publish

In terms of real objects created its something like this:

// Java
public class Widget extends Base {
  final String manufacturer;
  String serialNumber;

  public static Builder make(String model, String manufacturer) {
    return new Builder(model, manufacturer);
  }
  protected Widget(Builder builder) {
    this.manufacturer = builder.manufacturer;
    this.serialNumber = builder.serialNumber;
    super(builder);  // superclass checks nulls and locks const state
    safePublish(this);
  }

  public class Builder extends Base.Builder {
    public String manufacturer;
    public String serialNumber;

    public Builder(String model, String manufacturer) {
      model = model.toUpper();
      super(model);
      this.manufacturer = manufacturer
    }
    public Widget build() {
      verify();
      return new Widget(this);
    }
    protected void verify() {
      super.verify();
      if (manufacturer.length() < 2) throw new ArgErr();
    }
  }
}

The nice thing is that everything has its place. There are still three invokespecial hierarchies (the minimum necessary), but they all make sense - the constructor of the builder to setup the initial state from parameters, the builder verify() and the object constructor.

Now, the Fan syntax can be played around with of course. You don't have to have the new...verify concept. It could be three separate blocks randomly located in the source file. But located together its easier to understand what is going on and change it if necessary. Whichever syntax you choose in Fan, the end result in the builder would be similar.

For example, one tweak to the syntax would be to make safe publication explicit:

class Widget : Base {
  static new make(...) {
    new : super(model) {
      ...
    } verify {
      ...
    } publish {
      safePublish(new)
    }
  }
}

Login or Signup to reply.