-
Notifications
You must be signed in to change notification settings - Fork 1
Exploration: Immutability
BType has a number of immutable data types and structures (strings, tuples, etc.), and support for basic immutability on heap-allocated objects. This helps to substantially improve developers' ability to reason about certain application logic and provide guarantees about their code. However, all forms of immutability in BType at the time of writing do not affect the compiler's output and do not affect the behavior of the resulting application. That is, the compiler does not use immutability information to improve the application in any way other than to prevent developers from using a type incorrectly.
Update: This functionality was partially implemented, but is now being removed. This is happening for a few reasons:
- Object syntax
- Overloading the
final
keyword beyond the meaning that most developers are familiar with is confusing. - Objects are only ever implicitly immutable. Accidentally adding a non-final member would break the immutability of the object. This is a poor model to adopt.
- The
imm
syntax is confusing and limited.
One keyword will be added to the lexer: imm
. This keyword denotes an immutable type.
The parser will be changed such that any time a type is accepted, the imm
token will be accepted first. This will set a flag denoting immutability and then expect a type to follow.
During type validation, immutability will be checked. The following rules will be enforced:
- Immutability has no effect on primitives (
int
,str
, etc.) or tuples, as they are already immutable by default. The type validator may ban the immutable keyword in association with these types. - Members of immutable types may not be the target of assignment.
- Arrays may not be marked as immutable, since there is no way to create an array with data by default and their length is not known until runtime in many cases. Of course, arrays may contain immutable values.
- Functions (closures) cannot be marked as immutable as part of this proposal, as this introduces non-obvious behavior. Since the closure is only used in an immutable way (the
imm
keyword is on the type rather than the declaration), the function would, for example, be able to read values lexically but not be able to write them. This is a can of worms that is avoided by banning immutable closures. - If an object is used as immutable, its constructor and all of its members must be marked as
final
(defined below).
Immutable types are to be considered distinct from their mutable counterparts. That is, a value of type imm Foo
cannot be assigned to a variable of type Foo
.
Variables declared with an immutable type must be immediately assigned a value. The following declarations are invalid:
imm Foo:myVar1; # Invalid implicit `null`
imm Foo:myVar2 = null; # Invalid explicit `null`
null
is not a valid value for immutable types.
Values may be cast from immutable to mutable using the as
operator:
imm Foo:immutable = getFoo();
Foo:mutable = immutable as Foo;
Casting an immutable value creates a copy of the immutable value. The mutable value is not a reference to the original immutable value.
Mutable values may be cast to immutable as well:
Foo:mutable = getMutFoo();
imm Foo:mutable = mutable as imm Foo;
If the value being cast to an immutable type is null
, the zero-value of the object is created (all-null
references and zeroed primitives). This ensures that the variable always contains a value once it has been declared.
In order for a user-defined object to be used immutably, it must be defined with an immutable constructor and all final
members. To do this, the final
keyword will be expected before the new
keyword when defining an object's constructor:
object Foo {
final int:x;
final new() {
self.x = 123;
}
}
Constructors marked final
have the restriction that the self
parameter will not be allowed to leave the scope of the constructor. That is, the following code is invalid:
object Foo {
final new() {
processFoo(self); # Invalid!
}
}
func processFoo(Foo:bar) {}
All members on the object must be marked as final
in order for the object to be used immutably. If a method on the object is called, it must also be marked as final
. Methods marked as final
have similar behavior (cannot expose a reference to self
, all method calls must be to final methods).
Keep in mind that while func
types are somewhat immutable (the function that they point at and the lexical binding cannot be changed), the contents of the lexical binding may change. That is, the bound function or another function originating from the same scope may mutate a variable in the lexical scope, causing the values within it to mutate. This will happen even if the closure is marked as final
.
Immutable values will always be stack-allocated. Creating a new reference to an immutable value creates a copy.
By stack-allocating all immutable objects, there can be large performance benefits. Garbage collection is never invoked, and memory can be freed immediately when an immutable object goes out of scope.
Immutable objects allow the introduction of relatively safe parallelism in the future. Immutable objects can be passed safely between threads with no additional work needed.