2020/01/23

Coming from Java, Part II

(This topic is large, so this entry is just the second installment. This is Part II; here is a link to Part I.)

In Object Oriented languages, objects represent the combination of related state and behavior. Java classes declare fields to hold state, and methods to provide behavior. Java fields and the methods are nested immediately within the class that contains them. This is an example of a common pattern for a Java class exposing state, stored in fields, via property accessors:
public class Person
    {
    public Person(String name, String phone)
        {
        setName(name);
        setPhone(phone);
        }
    
    private String name;
    private String phone;

    public String getName()
        {
        return name;
        }

    public void setName(String name)
        {
        assert name != null;
        this.name = name;
        }

    public String getPhone()
        {
        return phone;
        }

    public void setPhone(String phone)
        {
        this.phone = phone;
        }
    }
Ecstasy classes do not declare fields; instead, Ecstasy classes have properties that represent object state. A property is like an object, in that it can have its own nested state, and its own nested behavior. For example, to obtain the value of a property, one can invoke the get() method on the property. If the property is writable, then one can modify the value of the property by invoking the set() method on the property. (Of course, it is possible to use the simple dot notation for property access, which means that explicit calls to get() and set()are unnecessary.) Here is the above class, re-written in Ecstasy:
class Person(String name, String? phone);
You could also write it out in long-hand if you prefer; the following code compiles to the same exact result as the above code:
class Person
    {
    construct(String name, String? phone = Null)
        {
        this.name  = name;
        this.phone = phone;
        }
        
    String name;
    String? phone;
    }
It's possible in Java to make the "getter" and "setter" have different access, such as:
public String getName()
    {
    return name;
    }

private void setName(String name)
    {
    assert name != null;
    this.name = name;
    }
To accomplish this in Ecstasy, the equivalent is:
public/private String name;
The first access, "public", specifies that the property shows up in the public type as a Ref<String>; a Ref represents a read-only reference to a value. The second access, "private", specifies that the property shows up in the private type as a Var<String>; a Var represents both read and write access to the value.

Remember, though, that a property is like an object. Let's expand the Java example slightly, to validate that the name is not an empty String:
public void setName(String name)
    {
    assert name != null && name.length() > 0;
    this.name = name;
    }
In Ecstasy, the property contains a method called set(String) that we can override:
public/private String name.set(String name)
    {
    assert:arg name.size > 0;
    super(name);
    }
The above is just short-hand notation for:
public/private String name
    {
    @Override void set(String name)
        {
        assert:arg name.size > 0;
        super(name);
        }    
    }
There are a couple of important points here:
  • It's not the set() method that is private. The set() method is public, because it is part of the Var interface, as explained above.
  • Instead, the public Person type (known as Person:public or Person.PublicType) has a property that does not have a set() method, while the private Person type (known as Person:private or Person.PrivateType) has a property that does have a set() method.
  • In Java, "super" refers to the super-class. In Ecstasy, super is a reference to the function (like a function pointer) that is next in line to invoke in the virtual method's invocation chain. In other words, super is a function.
  • While it's not directly related, you can read more about the various specializations of the assert statement on this blog. The assert:arg statement produces an IllegalArgument exception if the assertion fails.
The interesting thing, though, is that we never have to deal with the field. We know it's there, because it has to be in order to hold the value, but the field doesn't have a name, we don't access it, and we don't modify it. Instead, we just call the super function for get() or set(), and at the end of that chain there is some implementation of the method (that we didn't have to write!) that accesses or stores the value for us using the field.

But what if we made it so that we could never reach the end of those method chains?
public/private String name
    {
    String get()
        {
        return "Bob";
        }

    void set(String name)
        {
        assert:arg name.size > 0;
        // do nothing with the name ... do not store it!
        }
    }
In this case, there would be no field for the name property, because it's obvious to the compiler that one is not needed!

So now it should be obvious that fields exist in Ecstasy, but that we never really have to mess around with them. Where are those fields actually held, though?

In one of the examples above, we talked about how the Person class has a public type and a private type, so you probably already guessed that the Person class also has a protected type, and you would be correct!

But the Person class has one more type: the Person:struct type. The Person:struct type has one property for each property of the Person class that needs a field. We call each property on the struct type a "field"; in Ecstasy, a field is just a property on a class' struct type.

The struct type is not user-definable. The struct type is automatically calculated by the compiler at compile-time, and by linker/loader at run-time. While the public, protected, and private types all refer to the same underlying object -- as if they were three different lenses through which you can view the same object -- the struct, on the other hand, is a separate object that is an implementation of the Struct interface.

For the purpose of this article, this is already way too much low-level information about structs, but the details are important for one reason: To understand constructors.

Constructors are weird. They live in a zone between non-existence and existence. They play by some extraordinary rules in Java, and the same is true in Ecstasy, because they fit into a zone of unknowns. Here's what a constructor looks like in Java, just to pick one at random from our own prototype compiler that was written in Java:
public StringConstant(ConstantPool pool, String sVal)
    {
    super(pool);

    assert sVal != null;
    m_sVal = sVal;
    }
First, in Java, a constructor must call either a different constructor on this class, or a constructor on the super class. Then, it is free to do other stuff, like checking parameters and initializing fields. Fields that aren't explicitly initialized are all set to their defaults, which is easy when null is a sub-class of everything.

Ecstasy is different. Not necessarily simpler or more complicated. Not necessarily better or worse. But it is different for very purposeful reasons:
  • Contruction is treated as a finite state automaton. Eliminating unknowns and improving predictability of execution is extremely important, and that is exactly what a finite state automaton does.
  • There is a period of time before the object is constructed. The developer gets complete control over that process.
  • There is a period of time after the object is constructed. The developer gets complete control over that process.
  • In between the before and the after, the developer is completely absent, and completely out of the picture for the moment of creation. During that moment, all the rules of object instantiation can be verified, and the object is created. We say that "the this becomes existent".
In that period before the object creation, there are two phases that the developer can implement:
  1. The construct(...) function(s) allows the developer to specify what information is needed to initialize the state of the object, and the developer can validate that information and initialize the structure of the object, which is the aforementioned struct.
  2. The assert() function allows the developer to collect, in one place, any assertions (or any other last-second work) that must occur before the object is created.
Here is an example of a constructor, from the Date class, which simply delegates to another constructor using the construct keword:
construct (Int year, Int month, Int day)
    {
    construct Date(calcEpochOffset(year, month, day));
    }
For both construct(...) and assert(), the this variable is the struct  -- not the object, because it has not yet been created! After that code has all completed successfully, the struct is checked by the system to make sure that all necessary fields have been assigned a value, and then the object is instantiated based on the struct. Then -- after the moment of creation, and before the newly created "this" reference is returned to the code that invoked the new operator -- one more step occurs: The corresponding finally(...) function for each previously invoked construct(...) function is executed, so that the object itself gets to see itself (and finish anything that it needs to) before being returned to the code that requested it.

Here's an example from the Array class:
protected construct(ArrayDelegate<Element> delegate)
    {
    this.delegate = delegate;
    }
finally
    {
    if (mutability == Constant)
        {
        makeImmutable();
        }
    }
In this example, the construct(...) function fills in the fields of the struct, but because the Array object does not yet exist at this point, the construct(...) function can not call the makeImmutable() method on the Array -- until the Array actually exists! And that is the purpose of the finally function -- to allow the new Array object to perform behavior that must occur as if it were part of the instantiation of the object, before that object is returned to the code that requested the object to be created.

A class can define an assert() function (with no parameters) as well. Regardless of whether any particular construct(...) function is invoked by the new operator, or by a sub-class -- and note that a sub-class is not required to invoke any construct(...) function on its super-class! -- the assert() function will be invoked before the object is created.

There are many details regarding the specific order of execution, handling of exceptions, and so on, but this post hopefully has given you a glimpse into how object structure works in Ecstasy, and how Ecstasy objects are created.

(Continue to Part III.)

No comments:

Post a Comment

All comments are subject to the Ecstasy code of conduct. To reduce spam, comments on old posts are queued for review before being published.