Thursday, September 15, 2011

Fantom: Constructors, Required Fields, and More

This post is part of a series of sorts where I write about what I find while exploring the Fantom language. This time I'm going to share what I find out about declaring and using classes as I go. I imagine I will only scratch the surface.

To give you the chance to devote some of your valuable reading time elsewhere, below is a summary of some of the highlights covered below:

  • Declaring class fields that are required at object creation and throughout the lifetime of the object
  • Default class field values
  • Automatic getters and setters

A class can be declared using the class keyword as we're all probably used to. Let's declare a (boring) class Person with a name field and add a main method so we can try and create an instance.

Assignments in Fantom are done via the use of := instead of an equals sign. Here we'll start by simply creating an empty object and printing out to the console that we've succeeded:

    class Person {
Str name

static Void main(Str[] args) {
Person p := Person()
echo("Person created.")
}
}

The variable p is given what looks to be the result of a call to a default constructor. We can run the above with:

   $ fan Person.fan

Our first run gives us the following error:

   Non-nullable field 'name' must be assigned in constructor 'make'

The error hints that the call to Person() results in a call to a constructor named make. Also, the error descrption seems to go against what we may be used to in Java, where class properties can be null. Right of the bat to me, this could demonstrate a potential strength of Fantom as, as given the class definition above, a user cannot create an incomplete Person. If we want to create a Person without assigning a value to the name field, we can append a '?' to the field declaration:

    class Person {
Str? name

static Void main(Str[] args) {
Person p := Person()
echo("Person created.")
}
}

This time we'll get the "success" message indicating we've created a Person. Let's keep the name field required and assign a value to it when the Person is constructed. To do this, we can add a constructor and in the main method create a Person named 'Cosmo':

    class Person {

Str name

new make(Str name) {
this.name = name
}

static Void main(Str[] args) {
Person p := Person("Cosmo")
echo("Person created with name: " + p.name)
}
}

As you can see, the main method now also reports the name of the created Person via a call to the name method. Although it looks like we are accessing the field directly, getters and setters are automatically created for each field which helps us cut down on the amount of boilerplate code our class might contain, not unlike some of the newer JVM langauges. Running things through the interpreter again gives us:

    Person created with name: Cosmo

To demonstrate the generated setter, we can add the following:

    p.name = "George"
echo("Person renamed to: " + p.name)

The output of our program is now:

   Person created with name: Cosmo
Person renamed to: George

One question I now have is now that we have declared the name field as non-null at construction time, can we assign the value to null after the object has been created? Let's add the lines below to the main method:

    p.name = null
echo("Person with null name?: " + p.name)

With the above in place, we now get an error:

   'null' is not assignable to 'sys::Str'

Again, this restriction should in practice help keep our class instances safe and remove the usual null-checking concerns.

Now some might say a class with one field might be pretty boring (if not useless). Lets add another field, age, but not require a user of the class to do a Person's age at construction time by allowing a default value (since age can be a touchy subject anyway). Let's try adding another constructor to be used when both the name and the age of the Person are known:

    class Person {

Str name
Int age := 1

new make(Str name) {
this.name = name
}

new make(Str name, Int age) {
this.name = name
this.age = age
}

static Void main(Str[] args) {
Person cosmo := Person("Cosmo", 40)
Person marshall := Person("Marshall")
echo("Person " + cosmo.name + ", aged " + cosmo.age)
echo("Person " + marshall.name + ", aged " + marshall.age)
}
}

Here will create two Persons with an declared age and one with the default of 1, and report on both:

    Duplicate slot name 'make'

Ah, we get an error... are we not allowed to have two constructors? Turns out in Fantom, constructors are treated like methods, with the new keyword out in front, and can be either be named make or start with make. Since we will likely prefer for our users to fully populate the class fields, let's name the two parameter constructor make and rename the original:

    new makeNamed(Str name) {
this.name = name
}

new make(Str name, Int age) {
this.name = name
this.age = age
}

Here though, how do we differentiate between which constructor is used for each assignment? The Persons we create are made through a call to Person(...)? Turns out we can call the constructors by name by referring to them through the class definition:

    Person cosmo := Person.make("Cosmo", 40)
Person marshall := Person.makeNamed("Marshall")

We cannot it appears make calls to Person(...) like we did before and have the compiler infer with constructor to use asin:

    Person cosmo := Person("Cosmo", 40)
Person marshall := Person("Marshall")

This results in the error:

   Invalid args (sys::Str, sys::Int), not (sys::Str)

Back to the explicit calls, our class now reads:

    class Person {

Str name
Int age := 1

new makeNamed(Str name) {
this.name = name
}

new make(Str name, Int age) {
this.name = name
this.age = age
}

static Void main(Str[] args) {
Person cosmo := Person.make("Cosmo", 40)
Person marshall := Person.makeNamed("Marshall")
echo("Person " + cosmo.name + ", aged " + cosmo.age)
echo("Person " + marshall.name + ", aged " + marshall.age)
}
}

Giving this attempt a shot gives us...

   Person Cosmo, aged 40
Person Marshall, aged 1

...output for both Persons, with the second making use of the default age.

This looks like enough to cover for now in one post. Looking at the documentation further, there's looks like there's a lot more to cover at a later date. If I find I misstated any of the terminology or made things more difficult then they need to be, I'll be sure to come back and make corrections.

No comments:

Post a Comment