ballerina-platform / ballerina-spec

Ballerina Language and Platform Specifications
Other
168 stars 53 forks source link

Should object fields be declared as final rather than readonly? #580

Closed jclark closed 4 years ago

jclark commented 4 years ago

Currently, we allow object fields to be declared as readonly, in the same way as record fields. I am wondering whether it would be better instead (or perhaps in addition) to allow them to be declared as final, for the following reasons:

sameerajayasoma commented 4 years ago

It is quite useful to allow final in an object field declaration to create an immutable binding. Allowing only readonly can make specific patterns impossible to implement with Ballerina objects. E.g., Having map value with lazy initialization with an immutable binding to an object field.

readonly Person p; is sugar for final readonly & Person p;. I think we should only allow final in object fields.

If we allow both final and readonly there, then we should allow readonly in module-var-decl and local-var-decl-stmt to be consistent.

jclark commented 4 years ago

A number of upcoming features really need final fields on objects, in particular isolated objects and resources.

However adding them is not so straightforward. Although the concepts of readonly and final are superficially similar, there are fundamental differences.

readonly is a concept that:

whereas final:

The concept of readonly record fields is that part of the record is readonly, i.e. that field and the value it contains are readonly. Whether a field is readonly is part of the shape of the record, i.e. the shape of a record has a read-only bit for every field. The inherent type of a readonly mapping is a singleton (effectively readonly mappings don't have an inherent type); similarly, a record's inherent type for a readonly field should allow only a singleton. (Objects don't have this concept.)

I am reluctant to allow final as well as readonly for object fields. It feels like too much complexity.

So the following feels like the best compromise to me:

  1. An object field can be declared as final: this is part of the class of the object. The class of the object controls how the object can be mutated. Whether an object field is final is not part of the shape of an object.
  2. Get rid of the concept that the shape of an object includes a read-only bit for individual fields. An object field cannot be declared as read-only.
  3. The shape of an object still has a read-only bit for the entire object. A class definition can still use readonly to make the whole object read-only. If a class is readonly, then all its fields will be constrained to be a subtype of readonly and its fields will effectively be final.

With this design, object fields will be consistent with module-level variables, but different from record fields.

Relative to the current design, the change is:

sameerajayasoma commented 4 years ago

How does the "finalness" of an object field effects subtyping? As per my understanding of this proposal, the "finalness" of an object filed is not part of the shape of an object. Now consider two classes A and B where A is a subtype of B. The field foo in A is declared as final field and the field foo in B is not declared as final. Now if I assign a value of class type A to a variable of type B, I can replace the value of foo with another value.

jclark commented 4 years ago

We approved this proposal https://github.com/ballerina-platform/ballerina-spec/issues/580#issuecomment-674377182 in today's meeting.

jclark commented 4 years ago

Note that this means that only classes and object constructors can define fields as final: it doesn't make sense for an object type descriptor to do so.

MaryamZi commented 4 years ago

@jclark, would appreciate clarification regarding subtyping with final fields.

I have the same concern raised by @sameerajayasoma in https://github.com/ballerina-platform/ballerina-spec/issues/580#issuecomment-674457913. Would the example there result in a panic at runtime when trying to set a value to the final field?

Similarly, I have a concern with the opposite assignment - a value of a class with a non-final field to a variable of a class with a final field.

Just as a high-level example, can a Bar value below be used as a Foo value?

const map<int> CONST_MAP = {
    a: 1,
    b: 2
};

class Foo {
    final map<int> x;
    boolean y = true;

    function init(map<int> x = CONST_MAP) {
        self.x = x;
    }
}

class Bar {
    map<int> x = CONST_MAP;
    boolean y = false;

    function resetX() {
        self.x = {}; // sets a new map value to `x`
    }
}

public function main() {
    Bar b = new;

    _ = start processFoo(b); // is this valid? can I use a `Bar` as a `Foo`?

    b.resetX();
}

function processFoo(Foo f) {
    if f.x === CONST_MAP {
        // Do something if field `x` is `CONST_MAP`,
        // assuming it would not change since `x` is a final field in `Foo`.
        // But while here, someone can call `b.resetX` and
        // reassign a new value to `x`.

        int i = f.x.get("a"); // Shouldn't panic based on the assumptions
                              // since `CONST_MAP` has a field `a`.
    }
}
jclark commented 4 years ago

final isn't part of the type, so @sameerajayasoma's example in https://github.com/ballerina-platform/ballerina-spec/issues/580#issuecomment-674457913 would panic. It also means that, in your example, Bar is a subtype of Foo. The author of processFoo cannot reliably make assumptions about f that are not part of the type of Foo (including the final-ness of fields). In other words, class Foo defines a type and defines an implementation of that type; declaring the parameter to processFoo to be Foo means only that the parameter must belong to type Foo. It doesn't mean that it will have the implementation defined by class Foo. That's structural typing. If you don't want that, then make Foo distinct.

MaryamZi commented 4 years ago

This is clear now. Thank you.