Immutability in Ballerina — Part I

Maryam Ziyad
Ballerina Swan Lake Tech Blog
6 min readSep 26, 2021
Photo by Aaron Burden on Unsplash

Ballerina provides first-class support for immutability.

Immutability in Ballerina is deep; a value that is immutable is guaranteed to hold/refer to only immutable values. In other words, a value accessed via an immutable value is also guaranteed to be immutable.

Immutability in the type system can be leveraged to provide concurrency-safe access to shared data. Moreover, it can be used to enforce restrictions on the kinds of values that can be provided (e.g., by making the type of a parameter be an immutable type) and to also convey via the API itself what actions may return immutable values. Additional compile-time checks may also help reduce runtime surprises with invalid attempts to update immutable values.

In this blog post we will be looking into the following key features that facilitate immutability in Ballerina.

  • the readonly type to represent immutability in the type system
  • constructing a value as an immutable value
  • constructing an immutable clone of a mutable value
  • constants
  • typing with immutable values

Moreover, we will also be looking into how final variables differ from constants.

This post is based on the beta3 release of Ballerina Swan Lake, but is expected to work with subsequent versions too.

The `readonly` Type

The readonly type in Ballerina represents all immutable values.

A value belongs to the readonly type only if it is immutable. All immutable values belong to the readonly type.

Ballerina defines the following basic types as inherently immutable types.

  • simple types — nil, boolean, int, float, decimal
  • string
  • error
  • function
  • typedesc
  • handle

A value that is of one of these types is always immutable and thus, will always belong to readonly.

The following basic types are defined to be selectively immutable.

  • xml
  • list — arrays and tuples
  • mapping — maps and records
  • table
  • object

Values belonging to these selectively immutable types may or may not be immutable. Such a value will belong to the readonly type only if it is an immutable value.

Selectively immutable types can be combined with the readonly type in an intersection type to represent immutable values of the particular selectively immutable type.

For example, int[] represents all integer arrays in Ballerina. A variable of type int[] may hold a value that may or may not be immutable, so you cannot directly use such a variable in a context where an immutable value is expected.

In order to represent only immutable integer arrays we can use the intersection type int[] & readonly. A value will belong to an intersection type T1 & T2 only if it belongs to both T1 and T2. So in this case a value will belong to int[] & readonly only if it is an integer array and if it is immutable. Therefore a variable of type int[] & readonly can be used in a context where an immutable value is expected.

Thus the readonly type can be used, both standalone and in conjunction with selectively immutable types, to represent (and enforce) immutable values.

The compiler will also validate that there are no attempts to modify an immutable value. An attempt to do so will result in a compilation error.

Update attempts on values that may or may not be immutable will be allowed at compile-time, but at runtime, if the actual value is immutable the update attempt will panic.

Running this will result in abrupt completion with a panic.

Constructing a Value as an Immutable Value

A value can be constructed as an immutable value by providing an intersection type with readonly as the contextually-expected type for a constructor expression.

Alternatively, the same can be done using a type cast expression with readonly. Here again the applicable type used in the construction of the value will be the intersection of the contextually-expected type and readonly.

As demonstrated in the example, the is expression can be used to check if a value is immutable.

A future version of jBallerina will allow using the is expression with just readonly to test immutability.

Creating an immutable clone of a mutable value using the`value:cloneReadOnly` langlib function

The value:cloneReadOnly langlib function can be used to create an immutable clone of a mutable value; this basically creates a copy of a value marking it as read-only.

This function also guarantees deep immutability by recursively copying members of a sequence or structure as immutable values.

Calling .cloneReadOnly() on an immutable value does not result in a copy, instead the value itself is returned.

Creating an immutable value from a mutable value using the`value:cloneWithType` langlib function

The value:cloneWithType langlib function can also be used to create an immutable value from a mutable value, similar to the value:cloneReadOnly function.

However, unlike with value:cloneReadOnly, value:cloneWithType may perform additional operations such as numeric conversions and using default values for record fields to make the conversion work.

Constants

Constants in Ballerina declare named immutable values known at compile time.

const int MAX_COUNT = 10;

A value can be specified for a constant only in the constant declaration and it is a compile-time error to assign a new value to a variable reference when it refers to a constant (i.e., they are effectively final).

When the constant is a structured value, any and all members of the structure should also be constants.

In addition to it not being possible to assign a new value to the constant, the constant value itself cannot be updated.

All constants should belong to anydata.

The effective type of the constant is the intersection of readonly and the singleton type representing the value declared for the constant. The type used in the constant declaration is only used to interpret the constant expression but the actual type of the constant will still be the intersection of readonly and the singleton type.

It is also possible to omit the type in a constant declaration.

const MAX_COUNT = 10;

Since a constant value is known at compile-time a reference to a constant can be used contexts that expect values known at compile-time (e.g., in a type-descriptor or a match pattern).

Note: the jBallerina implementation currently provides limited support for constants as at Swan Lake Beta2/Beta3. A future version will provide complete support as defined in this section.

`final` Variables

A final variable can be assigned to only once.

Unlike constants, final variables can be declared locally and can also be initialized separately.

A final variable only guarantees that the variable is not assigned to after initialization. Whereas with constants, in addition to guaranteeing that there are no assignments after initialization, it is also guaranteed that the value itself doesn’t change.

With final variables, whether or not the value can be updated (in the case of structured values) depends on the type of the final variable.

For example, if the type of the final variable is mutable, it may be possible to still update the value even though it is not possible to assign a new value to the variable.

However, if the type of the variable is a subtype of readonly, update attempts will also be disallowed, since the value itself is immutable and cannot be updated.

Thus, even though there are some similarities between constant and final variables, they are fundamentally two different concepts. A constant is comparable to a final variable if and only if the type is a subtype of anydata & readonly and the value is known at compile-time.

The following are implicitly final in Ballerina

  • function parameters
  • variables bound in the typed-binding pattern of a foreach statement
  • variables bound in let variable declarations
  • variables bound by the clauses of a query
  • configurable variables

Non-final Variables of Immutable Types

Given that the final qualifier defines whether or not a variable can be assigned to after initialization, a non-final variable may be assigned a new value even when it’s type is a subtype of readonly. The only constraint is that the new value is also immutable.

Typing with Immutable Values

Since an immutable value cannot be updated, immutability affects whether or not a value belongs to a specific type.

For example, if you have a mutable array with inherent type anydata[] that has only int values, using the is check with the array for int[] will return false, since even though at that point the array has only int values it may be modified in a way that it no longer contains only integers.

But if the value is immutable, the is check will evaluate to true since the value cannot be updated to have a structure different to int[].

Hope you found this useful! In Part II we will be looking into

  • readonly record fields
  • final object fields
  • readonly classes

Until next time!

--

--