#249 Constructor suggestion

cbeust Fri 20 Jun 2008

Hi everyone,

I had a private discussion with Brian, which he asked me to post here to gather some additional feedback.

I was pointing out that I wasn't a big fan of Fan's current way of defining constructors for a couple of reasons:

  1. It relies on a convention ("constructors should start with make"), so it can't be enforced by the compiler and it might require some work on the developer's part to find out how to construct objects of a class they are not familiar with
  2. It's not symmetric: you use new to declare a constructor and makeXXX to create an object.

I pointed out that it would be possible to solve these two problems with one unique notation, such as:

// Invalid Fan
class Point
{
  new(Int x, Int y) { this.x = x; this.y = y; }
  Int x
  Int y
}

p = Point.new(2, 3)

Here, we use new to declare a constructor and new to instantiate an object. Also, we don't have to guess what the constructors are called.

Obviously, this would require Fan to implement "full" overloading.

So here are my questions to the audience:

  1. What's your general feeling about the syntax above (or a similar approach that would address points a) and b))?
  2. Can you think of a way to address these points without requiring full support for overloading?
  3. Should Fan support full overloading?

-- Cedric

andy Fri 20 Jun 2008

I agree new would be nicer - but as you mentioned, that would require overloading. There have been cases where I've wanted overloading, but I think the simplicity of the resulting type system and reflection trumps that.

Also, one of the other nice things about that make convention is it combines constructor and factory methods into a consistent style:

class Foo
{
  new make() { x = 5 }
  Int x := null    
}

class Bar
{
  static Bar makeBar() 
  {
    if (bar == null) bar = new Bar()
    return bar
  }
  private new make() { y = 5 }
  private Bar bar := null
  private Int y   := null
} 

foo := Foo.make
bar := Bar.makeBar

I know this isn't the best example, but you should get my point, I personally think thats a nice side-effect, versus using new sometimes, and static methods other times.

cbeust Fri 20 Jun 2008

Interesting, it does allow migrating from a constructor to a factory without breaking user code (well, at least at the source level, how about at the binary level?).

JohnDG Fri 20 Jun 2008

At least the new could be eliminated, reducing the confusion that exists now (make or new?). In fact, no need for a new keyword in defining the constructor, either. One way to do something is better than two, and a lot cleaner and simpler too.

cbeust Fri 20 Jun 2008

Actually, I think the new keyword is important since it's pretty much the only way for a developer (and the compiler) to tell if that method is a constructor.

Come to think of it, do we even need this notion of a constructor?

We could just say that "a constructor is a static method that returns an instance of the class it belongs to". Objective C tried to clarify this concept by separating the idea of allocating an object and initializing it, but I'm not sure we really need this.

-- Cedric

JohnDG Fri 20 Jun 2008

Exactly. No need for a separate concept of a constructor if you can call static methods that create instances of the object. If you want to ensure consistency, maybe require the no argument constructor be called make(), and that the other ones start with make, though I'm not sure if this would be needed.

tompalmer Fri 20 Jun 2008

I think the current style in Fan is OK. I'd love for it to be binary compatible with statics (both at the Fan and JVM/CLR bytecode levels). At least, I think I'd like that if there aren't subtle issues I've ignored.

brian Sat 21 Jun 2008

Should Fan support full overloading?

My stance on method overloading: I'm strongly opposed. Yes it is nice sometimes, and like Andy I sometimes wish I had it - but I really love what not having it entails: I can lookup methods with a simple string. This notion is baked into the runtime extremely deep. It allows dynamic dispatch to be a simple name lookup in a hashmap. If you've ever looked at the formal rules for method resolution in the Java Lang Spec, they are pretty wicked complex.

Interesting, it does allow migrating from a constructor to a factory without breaking user code (well, at least at the source level, how about at the binary level?).

This is currently source compatible, and kind of binary compatible. A constructor call versus a static call are different fcode opcodes, but will get compiled into the same Java/IL staticinvoke call. So client access is transparent, but subclass calls are not. A subclass has to have a super class constructor to call, and that can't be replaced with a static method. This is the major problem with constructors - client access is a static call, but subclass access must be an instance call.

We could just say that "a constructor is a static method that returns an instance of the class it belongs to".

Exactly. No need for a separate concept of a constructor if you can call static methods that create instances of the object.

The problem is that this doesn't solve the subclass problem. Subclasses have to pass their own instances up to the superclass constructor. This is what makes constructors unique: static on the outside, instance on the inside.

JohnDG Sat 21 Jun 2008

True, Brian, but that's an implementation detail -- I still don't see the requirement for the new keyword anywhere.

brian Sat 21 Jun 2008

It doesn't have to be new, but you do have to indicate somehow that a method is a constructor. It isn't a static method and it isn't an instance method - it must be clearly marked as a constructor. I'm don't personally like new, but we picked it because we couldn't think of anything else, and it was already a keyword in Java and C#.

harishkswamy Sat 21 Jun 2008

I agree, Class.new would be consistent and elegant, but I actually agree with Fan's design to keep things simple. Besides, its not very often that I have had the need to overload, and when I do there's always another, more appropriate, name for the overloaded method. Also, I happen to like the new keyword, it clearly states that this method returns a new object of this class.

tompalmer Sat 21 Jun 2008

You could always use something crazy like ctor instead of new, but I'm not so sure that would be a good idea. (And overloading is definitely nice to avoid when you can make a clean break and design it that way from the ground up.)

tompalmer Sat 21 Jun 2008

OK. Here's a thought. Drop custom constructors, as recommended by some others above. Every concrete class without an explicit static method named new gets one added by the compiler. This method calls any existing instance method called init, and the compiler gives it the same parameters automatically. If there's no init, the implied new also takes no args and only makes a new instance.

This style is almost exactly the same as used by Ruby and Python, and I think it works well.

Note that you could also make your own static new method to hide it or customize it. It could be binary compatible as any static method.

This does make it harder to know how to promise class invariants are met, but people seem to survive this just fine in Python and Ruby and for factory methods in Java.

brian Sun 22 Jun 2008

Coming from Java I really thought that Fan should have guaranteed constructor invariants and a clean way for subclasses to call their super class's constructor. But the more and more I use Fan, I'm finding that constructors aren't used much at all. For example the widget toolkit doesn't use any constructors, rather it relies on with-blocks:

Button { text = "Foo"; image = icon; onSelect = &handleButton }

A convention I started with classes like Point was to use default parameters to allow either make or with-blocks:

const class Point
{
  new make(Int x := 0, Int y := 0) { this.x = x; this.y = y }
  const Int x
  const Int y
}

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

My point is that if Fan convention starts to shun constructors in favor of with-blocks, then a lot of the complexity associated with constructors can be simplified (and I'm always in favoring of making it more simple if the tradeoffs are right).

helium Sun 22 Jun 2008

In my opinion a class should have exactly one constructor.

const class Point
{
  new (Int x := 0, Int y := 0) { this.x = x; this.y = y }
  const Int x
  const Int y
}

a := Point(10, 20)   // python-like
...

In combination with default parameters and with blocks this should be more than sufficient. And you can still create static methods that internally use that single constructor to create the same kind of interfaces you currently do.

brian Sun 22 Jun 2008

In my opinion a class should have exactly one constructor.

I'm not quite there yet, but I'm getting close.

In combination with default parameters and with blocks this should be more than sufficient. And you can still create static methods that internally use that single constructor to create the same kind of interfaces you currently have to.

The problem is still how to deal with subclasses. If you have only one constructor, you can wrap it with static methods for client access - but the subclass must use the one and only constructor. That might be ok, although it is definitely a big tradeoff. But now that I'm seeing how little constructors are used in Fan this is starting to seem like a reasonable tradeoff.

If we went this route what would the syntax for super class constructor calls look like?

class Point3D : Point
{
  new (Int x := 0, Int y := 0, Int z := 0) : super(x, y) { this.z = z }
  const Int z
}

class Point3D : Point
{
  new (Int x := 0, Int y := 0, Int z := 0) { super(x, y); this.z = z }
  const Int z
}

tompalmer Mon 23 Jun 2008

I think helium has the right answer on this topic even though it doesn't address the mixed-static/instance-method subject. I also suspect that parameter-less constructors will be most common. And default parameter values also does make this better in Fan than otherwise, too.

I'm more familiar with super() inside the constructor body because I'm more familiar with Java, but that's not the familiar way to everyone. I think either is fine.

I trying recommend trying the "one constructor only named new" thing and see if it works out.

brian Tue 24 Jun 2008

The problem is this feature is really expensive to "try out" - it will require reworking a lot compiler code and the entire codebase. So I'd like to make sure we have it thought thru before biting it off. But I think it warrants being tracked as a feature on the roadmap doc.

gizmo Tue 24 Jun 2008

If I had to choose against one of the two solution brian pointed in his last sample, I would definitively prefer the first one because it enforce writing the call to the super constructor as the first instruction.

On the other hand, I would really prefer to have the ability to launch the super call in the constructor body AFTER having done some work, as some range check on some input values, or wathever. I don't know how it fits the actual implementation of Fan, but when developping in Java that's what I'm used to do: create a static method that do some validation check or computation then call a private constructor and return the result.

cbeust Tue 24 Jun 2008

Exactly: function should dictate form.

If super() can be invoked anywhere in the constructor, then it shouldn't be any different from a standard statement and therefore, it belongs inside the body.

If super() has to be invoked as the first statement, then it should be extracted out of the body in a place where its special treatment will be obvious.

Brian, which one is it? (I'm guessing the former?)

-- Cedric

brian Tue 24 Jun 2008

Because we aren't really Java constructors in this way we could allow super to be called anywhere. Although there are some issues with that:

  • not having your super ctor called means your superclass fields are initialized, that can lead to some unexpected behavior
  • you could loose the guarantee that it is only called once (if you put it inside a loop); compiler could be smart but that's extra complexity
  • if we do with this route, then I could switch to Java/C# constructors and save some calling overhead and make it a normal ctor call in Java/C# code - but then we need to follow JVM/CLR rules

So I'd be inclined to say we require super ctor call first - its less powerful, but more predictable and safe. In which case I'd probably prefer C++/C# syntax that we are currently using.

So no one has any objections to just one constructor? That in itself seems a like it would be a contentious issue.

cbeust Tue 24 Jun 2008

I must have missed a turn, I didn't realize that we were now discussing the possibility to restrict Fan classes to just one constructor...

I suspect it would work most of the time but I'm scared that the few times where your class really needs more than one constructor (and they can't be accomodated with default parameters), then the overall design is going to take a big hit.

Brian, is there a technical reason why you are contemplating this possibility or are you just thinking about it in the spirit of simplifying the design of the language?

-- Cedric

brian Tue 24 Jun 2008

Just the spirit of simplifying. I think the basic premise is that we are using with-blocks a lot more than constructors, so do we really need more than one constructor? If not we can drastically simplify down similar to your original proposal (just without the overloading).

cbeust Tue 24 Jun 2008

Can you detail what you mean by "we are using with-blocks a lot more than constructors"? I fail to see the connection between with block and constructors...

-- Cedric

andy Tue 24 Jun 2008

@Cedric:

A very trivial example, but we almost always use the first convention now, especially when there are multiple ways you might want to initialize values. Its easier IMO than having a bunch of ctor/factory methods to make using the API easy to use. It also makes the code easier to read.

pt := Point { x=5; y=6 }   // new style
pt := Point.make(5, 6)     // old style

I'm still not 100% sold on the solo-ctor, but I'm open to it.

brian Tue 24 Jun 2008

In Java you typically use constructors to pass in initial state. Consider the various constructors to JLabel:

JLabel()
JLabel(Icon image)
JLabel(Icon image, int horizontalAlignment)
JLabel(String text)
JLabel(String text, Icon icon, int horizontalAlignment)
JLabel(String text, int horizontalAlignment)

Fan's fwt::Label only has one constructor, which is used with with-blocks. We can handle all JLabel constructor cases as follows:

Label {}
Label { image = icon }
Label { image = icon; halign = Halign.center }
Label { text = "hello" }
Label { text = "hello"; image = icon; halign = Halign.center }
Label { text = "hello"; halign = Halign.center }

The more Fan APIs I write, the more I find this convention works really well.

cbeust Tue 24 Jun 2008

I see. So there is actually no "with" keyword, and your approach is basically to allow named parameters for constructors.

JLabel is actually a good example of what I was afraid would not work well with the solo constructor approach: multiple constructors that are not easily captured with default parameters, but I see that named parameters are a very reasonable and readable way of solving this problem.

Of course, you can probably guess the next question: how about allowing named parameters for methods? (I would be mildly against them, but the inconsistency probably needs to be explained)

-- Cedric

brian Tue 24 Jun 2008

how about allowing named parameters for methods?

With-blocks aren't really named parameters - they are a general purpose feature that operates on any base expression to change the implicit target within their scope. I can use them with methods too:

a.b(c) { d(e); f() }

// really means
temp := a.b(c)
temp.d(e)
temp.f()

Named parameters would be nice, but it is a feature I can live without. You can use map literals as a poor man's solution:

func(["a":a, "b":b])

cbeust Tue 24 Jun 2008

I see. This is in effect very similar to Java, which allows odd syntaxes such as:

List l = new ArrayList<String>() {{
  add("c");
  add("d");
}}

Notice the double braces (in effect creating an anonymous class).

Interestingly, if Java supported Unified Collection Literals, the syntax could become:

List l = new ArrayList<String>() {{
  "c";
  "d";
}}

Now, this happens to work nicely since you wired CLU to the "add" method, but this would fail for maps, since the addition is then done with "put", which is why I offered to make the adding method specifiable by a facet instead of hardcoding it to "add".

-- Cedric

gizmo Tue 24 Jun 2008

you could loose the guarantee that it is only called once (if you put it inside a loop); compiler could be smart but that's extra complexity

What about having a similar behaviour as the "once" method? multiple call to the super constructor will render the reference to the same object without initializing the members a second time.

if we do with this route, then I could switch to Java/C# constructors and save some calling overhead and make it a normal ctor call in Java/C# code - but then we need to follow JVM/CLR rules

Does it means that you would also allow overloading in the constructor or just allow one constructor with default values?

not having your super ctor called means your superclass fields are initialized, that can lead to some unexpected behavior

Well, in that case, what about removing the concept of constructor? For me, the real problem with constructor is that there is no clear limit to what they are intend to do. It's just a matter of "best practices" to define whether we need to call a constructor or a factory, what kind of parameters should be initialized and which should have a default value,...

From my standpoint, I would simply remove constructors, execpt the empty "new" one, and just kept the with-block syntax.

  • You need a default value? Put it right into the class definition.
  • You don't like to know the internal structure of a class? use the implicit getter and setters within the with-blocks.
  • You need to do some computation? Use a factory.
  • You want a utility class? A Singleton? Define it as an object, instead of a class, like in Scala.

In fact, I've never seen any case where complexe constructors have some benefit, in terms or readibility or reusability of the code, compare to the inlined serialisation of objects, or the factories.

Please, if you find some case that could go against that, point them to me, I'm always happy to learn some new way of programming.

brian Tue 24 Jun 2008

Notice the double braces (in effect creating an anonymous class).

Jeez - I never knew you could do that! How long has that feature been available?

If we decide to do maps, then the parser could figure it out via the syntax (same way we know whether [...] is a list of map literal):

MyMap { key1: va1; key2: val2 }

Which would be interpreted as add(key1, val1).add(key1, key2). Not sure I'm ready to tackle that yet. Because in a lot of situations the key is part of the child object:

Node {Node {name="foo"}; Node {name="bar"} }

brian Tue 24 Jun 2008

@gizmo You need to do some computation? Use a factory.

Static factory methods can solve a lot of problems, but they can't be used with subclasses.

Constructors do fill a good role to guarantee invariant instance initialization. You can't do that with anything other than constructors, because of the subclass issue. So the issue is:

  1. is constructor invariant initialization really needed?
  2. if you think it is, can we live with just one constructor?

To me that is the heart of this issue.

JohnDG Tue 24 Jun 2008

It seems Fan is very close to eliminating constructors entirely. Close, but not quite there yet.

In my experience, constructors have four main functions:

  1. To give fields default values.
  2. To perform general data propagation, initialization and component wiring.
  3. To compute fields and values that are functions of user-supplied data (invariants).
  4. To perform computation.
  5. To place an object into one of several modes.

(1) is largely obsolete with languages that allow direct assignment of field defaults (i.e. all new languages, including Fan) and (5) is a design smell, leaving three valid uses of constructors.

Fan already supports (2) in a brilliantly clean fashion, using with blocks.

That leaves (3) and (4).

In theory, with once methods, there is no need for invariant fields, because their data can be returned by some method -- but it's a little awkward and possibly defect-prone, since the cached value depends on when the method was invoked.

A little syntax sugar would take care of (3) nicely:

Int someField1, someField2
invariant Int myInvariant = someField1 + someField2

Anytime someField1 or someField2 changed, it would lead to a recomputation of myInvariant (dependent slots can be easily detected by a traversal of the AST for the RHS).

(4) is arguably a design smell as well, but it's motivated by a good aim, which is to ensure that if an object is constructed, it's all ready to go -- no need to call some initialize method, which a programmer might forget to call or might call before setting some fields. Perhaps an alternate way of doing computation might involve user-directed execution of some methods after initialization:

@constructor
once Void initialize()
{
  // Do computation here
}

The problem is you don't know when to invoke such methods. In Fan, with blocks are being used to replace the function of constructors. Do you consider a with block invoked concurrent with construction of the object to be an essential part of its construction, and execute computational methods following the with block? For example, in the code:

Label { text = "hello"; halign = Halign.center }

Do you count the with block as part of the construction of the Label instance? If so, then perhaps you invoke any @constructor methods immediately thereafter.

Not sure. It's clear in any case that constructors will not have the same relevance in Fan that they do in other languages.

cbeust Tue 24 Jun 2008

There is one important area of constructors that is not covered by with blocks: mandatory initializations.

There needs to be a way to make sure that whenever a class is initialized, certain fields receive a user-passed value.

Maybe we could add a @required facet?

class Point {

@required Int x
@required Int y
Str comment // optional for creation
Int year := 2008  // also optional but with a default value

}

Or should it be a keyword?

-- Cedric

JohnDG Tue 24 Jun 2008

I really like that idea. Even now constructors are littered with code that checks for non-null values, as developers have a habit of passing null for fields they don't understand.

A @required facet or required keyword would let the compiler do the grunt work.

Of course, in order to make that work, you need to reinforce the notion that the creation-time with block is, in effect, a constructor -- which opens the way to calling certain annotated methods immediately after execution of that with block.

JohnDG Tue 24 Jun 2008

One more thought: it would be nice to use the with syntax for initializing a superclass. Maybe something like:

class Child : Base
{
  super()
  {
    baseField = "foo"
  }

}

Or more elaborately:

class Child : Base
{
  Str myField

  super()
  {
    baseField = myField
  }

}

Child { myField = "foo" }.baseField.toChars // "foo"?

brian Tue 24 Jun 2008

Maybe we could add a @required facet?

I dig that idea. We'd just have to come up with some easy to remember rules.

One more thought: it would be nice to use the with syntax for initializing a superclass.

I think as long as we had some type of initializer/constructor that should be easy to do.

helium Tue 24 Jun 2008

foo := Bar { xxx = 42 } { yyy = "text" } { zzz = 3.1415926 }

Old semantics: Create a "Bar" than set xxx, yyy and zzz.

New semantics: Create an object of class Bar with xxx being set to 42, than run the "@constructor"-method and than set yyy and zzz.

I don't like it. It looks like a with block but realy is not. Don't add strange exceptions to something otherwise consistent. There has to be some indication that this construction blocks are something different from with blocks, IMO.

gizmo Tue 24 Jun 2008

@cbeust There is one important area of constructors that is not covered by with blocks: mandatory initializations.

Depends on your mood but there is two opposite way to consider this.

Once is the python philosphy which tells that programmers are not stupid. In that case, you don't enforce mandatory initializations, because you "hope" that progammer will read the doc or the source code to understand what is really needed. While this may be works sometimes, programmers are, first of all, humans. And human is lazy and won't read the doc if he can avoid it.

On the other hand, you may take the approach of Eiffel and assume that you should not allow user, not only to forget some mandatory arguments, but also ensure he's entring meaningfull values. Consider, for example, a rectangle. It would be great to force the user to use only positive integer for x and y length. This would lead to adding facets like @range, @min/max, @length, ...

From my side, I would prefer the second option as it prevents more error propagation both at compile and run time, but having just a single @required would be useless to me regarding to another option that would be the ability to have non-nullable types.

JohnDG Tue 24 Jun 2008

Well, with a @required annotation, you could run the @constructor when all required fields had been set. Or, no doubt, there's an even better way I haven't thought of.

The with block notion is so powerful and clean that I don't like seeing dichotomous methods of constructing objects. It's confusing and detracts from the simplicity of the syntax.

tompalmer Tue 24 Jun 2008

Two different thoughts.

Thought 1: More extreme than @required would be to allow an arbitrary validate() method that checks that the object was constructed correctly. Similar to class invariant validations in Eiffel but run only after initial construction. (Also feels like Struts.) The default validate() could check certain predefined facets like @required. I'm not sure this is a good idea. Just throwing it out there.

Thought 2: However, if we also support not-null, then having a not-null field left undefined would make something automatically required:

class Point {
  Int x // Required
  Int y
  Str? comment // optional for creation
  Int? year := 2008  // also optional but with a default value
}

Just another alternative. Not going to promise it's a good idea, either.

Login or Signup to reply.