Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blending readonly and isolated #1244

Open
jclark opened this issue May 9, 2023 · 5 comments
Open

Blending readonly and isolated #1244

jclark opened this issue May 9, 2023 · 5 comments
Assignees
Labels
Area/Lang Relates to the Ballerina language specification Type/Improvement Enhancement to language design
Milestone

Comments

@jclark
Copy link
Collaborator

jclark commented May 9, 2023

The main purpose of readonly in Ballerina is concurrency safety. This is the reason why readonly in Ballerina is strictly transitive. A readonly value cannot directly or indirectly reference an value that is not readonly. This restriction ensures that passing a readonly value to another strand is always safe: it cannot create a data race.

Historically, the isolated feature was developed after the readonly feature. With the introduction of isolated, this restriction is stronger than needed. Specifically, if a readonly only object was allowed to refer to an isolated object, it would still be safe to pass a readonly value to another strand.

Let's use the term effectively readonly to mean like readonly but also allowing isolated objects. More precisely, we can define an effectively readonly value to be any of:

  • simple value
  • string
  • xml
  • error
  • effectively readonly list
  • effectively readonly map
  • effectively readonly table
  • isolated object
  • function

where an effectively readonly list, map or table is one where the cell of every member

  • is immutable and
  • contains an effectively readonly value.

An effectively readonly value would then be safe to pass to another strand.

We also need clone to work on effectively readonly values as it does on readonly values: clone(x) would return x if x is effectively readonly.

An important question is whether we need to have both readonly and effectively readonly. It would be nice if we didn't since this is all too complicated already. One place where we might need true readonly (as opposed to effectively readonly) is annotations.

This would be quite a deep and complicated change

@jclark jclark added Type/Improvement Enhancement to language design Area/Lang Relates to the Ballerina language specification labels May 9, 2023
@jclark jclark self-assigned this May 9, 2023
@jclark
Copy link
Collaborator Author

jclark commented May 9, 2023

This will also affect the definition of Cloneable.

@jclark
Copy link
Collaborator Author

jclark commented May 9, 2023

I am going to call the current readonly semantics true-readonly and the readonly semantics that allows isolated objects effectively-readonly.

I think the right direction is to loosen readonly so that it means effectively-readonly. The considerations that suggest this direction are:

  1. clone needs to work on effective-readonly values, which means Cloneable needs to allow effective-readonly; but cloneReadOnly should continue to work on Cloneable (as a clone does), but it is going to be strange if cloneReadOnly doesn't return something that is readonly, but there's no way to make an isolated object true-readonly
  2. if readonly means effectively-readonly, it is easy to get true-readonly by intersecting with a type that does not allow objects (e.g. anydata)
  3. isolated objects are really quite similar to closures (function values constructed by anonymous function expressions); function values count as true-readonly so readonly already allows closures
  4. const expressions with a readonly type would still be true-readonly, because const-expressions do not allow syntax that constructs objects
  5. there are lots of the places where the spec already talks about "readonly or isolated object {}"

In other words, I think the spec should have one concept of readonly and that concept should be effectively readonly and thus allow isolated objects.

@jclark
Copy link
Collaborator Author

jclark commented May 9, 2023

If we try to make readonly mean effectively-readonly, we are going to have a problem with readonly objects vs isolated objects. These are two different things at the moment: a readonly object is a subtype of isolated object. But if readonly means effectively-readonly, readonly objects become a bit hard to make sense of.

Also if we define readonly to mean effectively-readonly, we won't have an easy way to describe true-readonly objects. Argh...

@jclark
Copy link
Collaborator Author

jclark commented May 10, 2023

Let's explore the other approach, where the meaning of readonly remains true-readonly. We need multiple changes to make this work.

More flexibility in declaring immutability of types

Allow ... part of record to have readonly modifier

At the moment, we allow record { readonly int x; } but not record {| readonly int...; |}; to make the rest part of the record readonly you have to make the whole record readonly readonly & record { int...; }. We would change this so that readonly can be applied to ... also e.g. record {| readonly int...; |}.

Allow final fields in record

At the moment we allow fields in objects to be final. This means the field itself cannot be assigned to, but does not affect the type of what is stored in the field. We would extend this so that record fields can also be final e.g. record { final Foo foo; } with the same semantic as for object fields. This would also apply to the ... field: e.g. record {| final Foo ...; |}.

Allow final/readonly for tuple members

Allow these for tuple members as we do now for record fields e.g.

[final Foo, final Bar]
[final Foo, final Bar...]
[readonly Foo, readonly Bar...]

Introduce a new value:Safe type to use instead of readonly|isolated object{}**

This would be defined like this:

type Safe readonly|record {|final Safe...; |} | [final Safe...]| | isolated object {};

In places where the spec now says "readonly or isolated object {}" it would say "Safe". In particular, an expression whose static type is a subtype of Safe is an isolated object.

Another possible name for this would be value:Isolated. It also might be possible to use the isolated keyword by itself for this (just as readonly is used both by itself as a type name, and as a modifier).

Making cloning work with isolated objects

Make the Clone and ImmutableClone abstract operations work with isolated objects. They handle isolated objects in the same way as they do readonly objects (including closes). Clone(v) and ImmutableClone(v) return v when v is an isolated object. (This might seem strange but Clone and ImmutableClone work this way for closures, and isolated objects and isolated closures are very similar: they may have internal mutable state, but all you can do from the outside is call them.)

This means value:Cloneable needs to be defined as:

readonly|xml|Cloneable[]|map<Cloneable>|table<map<Cloneable>>|isolated object{}

The return type of value:cloneReadOnly(T) will be T&Safe (rather than T&readonly as now). The cloneReadOnly name still makes sense because all the values that it constructs are readonly.

Notes

  • Safe doesn't allow isolated objects in tables. We could allow e.g. table<final map<final Safe>> to mean that the table and map are (at least) shallowly immutable (their members cannot be assigned to).

@jclark
Copy link
Collaborator Author

jclark commented May 10, 2023

@MaryamZi Please see if you can find any holes in the above proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area/Lang Relates to the Ballerina language specification Type/Improvement Enhancement to language design
Projects
None yet
Development

No branches or pull requests

1 participant