Plinth and Immutability

This post was going to be about immutability in Plinth, but I’ve just realised that I haven’t actually announced Plinth here yet. And so…

The Plinth Programming Language

Plinth is the name for a language I have been designing in my spare time for a while. Several of my posts in 2010 and 2011 were about it (then under the working name “Language X”). This year, I started work on an actual compiler for it that could produce executable machine code right from the start. I am currently in the process of building it up to a full language, and one of the more recent things I’ve implemented is immutability.

If you want to try out the compiler, or see updates as it’s being written, the github project is here: https://github.com/abryant/Plinth

And now back to the original topic:

Immutability

A few weeks ago, I was wrestling with how to implement immutability in a way that’s both safe and easy to use. The basic idea of immutability is similar to const in C++:

  • Types can be marked as immutable using a # prefix.
    i.e. #Foo is an immutable Foo
    Any object f with this type can not have any of its fields modified, so if f had a field b of type Bar, then f.b = null would be illegal.
    Plinth has “deep” immutability, which means that changing any fields of f.b would also be illegal.
  • Methods can be marked as immutable using the immutable modifier.
    When a method has this modifier, it cannot make any changes to anything that might be accessible elsewhere. This means that changing member variables and a static variables is forbidden, as is calling any other methods which are not immutable.
  • Constructors must also be marked as immutable if they are to be called from immutable methods. An immutable constructor can modify member variables, but not static variables.

There are several types which can be marked as immutable:
Arrays, e.g. #[]uint
Class types, e.g. #Foo
Compound types, e.g. #bigint
Function types, e.g. #{uint -> string}
The object type: #object

For arrays, classes, compound types, and the object type, being immutable means that the values inside that variable cannot be changed (e.g. an array element cannot be overwritten); for these four, casting away immutability is forbidden.

However, for function types, it means the opposite: that the function does not modify any external state, so it is an immutable function in the same way methods can be immutable. Because of this difference, function types can be cast away from immutability, but they cannot be cast towards immutability. For example:

#{string -> void} func = someMethod; // fine if someMethod is immutable
{string -> void} f2 = func; // fine, func doesn't change anything
#{string -> void} f3 = f2; // error: f2 might change something

Aside: With this type of immutability, making an object cache the result of some calculations would be impossible. For this reason, fields can be marked as ‘mutable’, which means that they can be modified (assigned to or have their state changed) from within an immutable context. As an example, this type of caching is used in Plinth’s string type, to lazily find its UTF codepoints.

Now, consider what happens when we write a getter (properties will exist eventually, but suppose we want to write a method):

class Foo
{
  // ... constructor ...
  Foo next;
  immutable Foo getNext()
  {
    return next; // error: next is immutable in this context, and cannot be returned
  }
}

So we can’t write an immutable getter that would extract a non-immutable member. We would have to write two getters, one which returns immutable values, and another for non-immutable values. This is extremely inconvenient, both in terms of writing duplicated getters and disambiguating between the two of them.

One way to try to get around these inconveniences is to have a concept of contextual immutability, which is added whenever you access a member in an immutable context. Then, immutable methods could return contextually immutable things, but not explicitly immutable things, allowing us to return next as a Foo rather than a #Foo in this case, and working around the duplication. Then, the result of the method call could have the same immutability as the thing we are calling the method on.

Unfortunately, this presents a problem for factory functions:

class Factory
{
  immutable Foo createFoo()
  {
    return new Foo();
  }
}
// in some method:
#Factory factory = ...;
Foo created = factory.createFoo(); // error: the result is immutable, because factory is immutable

Here, it is impossible to have an immutable Factory create a Foo which is not immutable. This is because we assumed that immutable methods would always return members, but in cases like this they do not.

In order for this to work properly, we would need to add another modifier to the method, stating that it can return contextually immutable values. In my opinion, this would overcomplicate method calls, so I am leaving them to return either always-immutable or never-immutable values.

However, the confusion here does not apply to properties, since they are designed to be accessed as if they were fields. So we can allow property getters to return against contextual immutability:

class Foo
{
  Foo realNext;
  property Foo next
    getter { return realNext; } // works, despite realNext being a #Foo in this context
    setter { realNext = value; };
  // NOTE: we could just do "property Foo next;" instead
}
// in some method:
Foo foo = ...;
Foo next = foo.next; // getter can return a contextually immutable value
#Foo immFoo = foo;
#Foo immNext = immFoo.next; // immutable callee forces immutable result

This gives us the same functionality as it would in methods, except that property getters cannot be used as function values (but even then, you can wrap the access in a closure if necessary).

Hopefully I’ll write about Plinth development more often from now on, since I’ve been getting a lot of the core language implemented recently. The next topic will be something that Plinth does differently from a lot of mainstream languages: nullability.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.