The subject of immutability sparks intense interest among the people who follow our blog, as is evident from the comments to the recent post by Niklas. Naturally, inside our team, a lot of thinking goes into making parallelism safe, and the ideas about restricted types play a major role in that. The work in this area was spearheaded by Joe Duffy, and I highly recommend watching this Channel 9 video where he gives an overview of the motivation behind his thinking.
In this post, I want to give the readers some insight into why we’re so interested in immutability, and specifically how it applies to Axum as a language.
Recall our earlier discussion on isolation and reader/writer agents. An example from the post went like this:
private int data1;
private string data2;
reader agent A : channel C
int n = parent.data1;
string s = parent.data2;
The agent is declared a reader, and the code in the constructor reads the domain state. All is well.
Following the same logic, I want to read a field of type Employee, so I add a new domain field employeeOfTheMonth and put the following in the agent’s constructor:
Employee employee = parent.employeeOfTheMonth; // 1
If this doesn’t seem suspect yet, let me continue. Remember, we’re still in the reader agent:
employee.Salary += 10000; // 2
Whoa! We’ve just modified a domain field from a reader agent – doing exactly what reader agents are not supposed to do. Clearly, we want the Axum compiler to flag these kinds of problems, and for that, either line #1 or line #2 should produce a compile-time error. At the same time, the example above must compile without errors.
Fortunately, line #1 does produce an error. The assignment fails, because the type of the right-hand-side expression is not Employee but a read-only Employee. The idea of a read-only reference is similar to const in C++ in that the read-only data cannot be changed, and a read-only reference cannot be converted to a “regular” reference. For an agent that is a reader, the parent reference is typed as a read-only reference, and that is why the fields reachable through it cannot be mutated.
The syntax of the read-only reference is immaterial at the moment, and fortunately most of the time you don’t need to use it, because you can use type inference – the var keyword:
var employee = parent.employeeOfTheMonth; // OK!
This gets you past the error. However, now the second line has to fail, and it does:
employee.Salary += 10000; // Error: cannot modify a // field via a read-only reference
Now what about int and string from the example above?
For ints, and types that behave like ints – types that exhibit value semantics – the read-only modifier simply doesn’t apply. For immutable types such as string, read-only modifier isn’t needed because it would be overkill – since the type itself is known to be immutable, the ability to declare read-only expressions of such type is superfluous – there simply isn’t any other kind!
This is why a reader agent can, in fact, safely read an int and a string – because there is no risk that it will mutate any objects reachable from them. (Of course, a reader agent still cannot mutate the object itself – a statement like “parent.data1 = 10;” would trigger a compiler error)
While it is clear that the idea of separating read-only and read-write access to data is fundamental in Axum, this remains an area of active research and experimentation. Being a new – and so far experimental – language, we’re willing to entertain some far-reaching ideas. For example, some members of the team urge us to take a step towards the world of functional programming and make read-only the default.
If we were to take that step, the assignment at line #1 above would work as originally written, and you would have to use a special syntax to create a writeable reference. This opens up some opportunities for us, but also makes it harder for people with C# background to adopt Axum. This is one of the areas where feedback from readers would be incredibly valuable.
To give you an example of such an opportunity, consider the following snippet of code from a reader agent:
Employee employee = GetEmployeeFromSomewhere();
target <-- employee;
Now if the target were a part of the dataflow network in the same domain, we would want to send the data by reference (without deeply cloning it), and have that dataflow network start processing the data immediately. This would only be safe if could guarantee that the agent that sent the data won’t be mutating that data while the dataflow network is reading it.
The employee being read-only would provide such a guarantee. Not having to say it in the code would make the guarantee, and the parallelism enabled by it, the default.