Closed jclark closed 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.
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:
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:
final
rather than readonly
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.
We approved this proposal https://github.com/ballerina-platform/ballerina-spec/issues/580#issuecomment-674377182 in today's meeting.
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.
@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`.
}
}
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.
This is clear now. Thank you.
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 asfinal
, for the following reasons:final
seems better thanreadonly
when something is assigned in an init method.final
but containing a mutable value (for example, in the desugaring of hierarchical resources).