A design of shared mutability

So far values in Roto we’re always passed by value, meaning that each function essentially gets a copy of each of its parameters. That is a very good default! For example, we want integers to be passed by value:

fn foo() {
    let x = 1;
    bar(x);
    # x should still be 1!
}

fn bar(x: i32) {
    x = x + 1;
}

However, this becomes difficult with complex types and data structures. I’m currently working on lists, which you often want to pass around to functions to let them modify the list in place.

Many scripting languages take the position that whether a type is passed by value or by reference depends on the type. Python, for example, passes integers by value and lists by reference. It also has a secret third option: types that are passed by reference but immutable (strings, for instance).

The downside of that approach is that it requires knowledge (and documentation) on every type in the language and it can lead to some unintended behaviour. One might for instance pass a list to a function that expects to be able to mutate the list freely, while the caller expects it to be unchanged.

Therefore, my goal is to build a mechanism into the language such that any type can either be passed by mutable reference or by value.

Design 1: ref bindings

The current design for this that I’ve got planned (but am not close to implementing yet, so lots of time to bikeshed) is ref bindings. Here’s how it works.

We start by making all types semantically pass-by-value. They might internally be passed by reference, but they will be immutable so that behavior cannot be observed from Roto.

If a function wants to mutate a value, it will need to specify that it takes a ref binding like so:

fn bar(x: ref u64) {
   x = x + 1;
}

This might feel similar to Rust’s references but there are no lifetimes. Then the caller needs to first make a ref binding to pass to bar.

fn foo() {
   let ref x = 1;
   bar(x);
}

Now the x created in foo will be updated by bar.

Ok, so why this design and not something more like references in Rust?

The key insight is that we don’t want to burden Roto with lifetimes. As a result, we will essentially need to put each ref value in a Arc<Mutex<_>>. Creating that safely means cloning the value into the arc. That means that just having a ref operator like this wouldn’t work:

fn foo() {
   let x = 1;
   bar(ref x);
}

Roto’s clone-all-the-time semantics would dictate that x gets cloned, then get wrapped into Arc and only then passed to bar! So it wouldn’t share the value at all.

By only allowing ref on bindings, we ensure that the Arc gets created directly when the variable is created, so that it can then be shared.

The neat part is that this opens up some possibilities that aren’t (easily) possible in other scripting languages, such as (like in the example) a shared reference to an integer.

We also need to think about allowing runtime functions to access these ref values, which won’t be easy either.

Once this is all implemented, we could also spend some time thinking about optimizing the Arc<Mutex<_>> away, but I think it’s safest to start with a simple implementation first that really makes the semantics here clear.

Design 2: Mutable value semantics

We could also implement mutable value semantics: https://www.jot.fm/issues/issue_2022_02/article2.pdf.

What this comes down to is that references are “second-class” meaning that they are not real types. Instead, only function arguments can be references. Here’s how that could look in Roto:

fn bar(ref x: u64) {
    x = x + 1;
}

fn foo() {
    let x = 1;
    bar(x); # or bar(ref x)
}

The ref here is specifically part of the function signature and function call syntax, not a full-blown operator. It should be thought of as syntax sugar for taking the value and then assigning the mutated value back to the original binding.

It does require a check that each value is only passed once. For example, this is not allowed:

bar(x, x)  # not allowed in MVS

The advantage of this design is that it’s well studied and that it doesn’t require putting things into Arc<Mutex<T>> behind the scenes. The authors of the paper claim that this enables pretty high performance at a low complexity cost.

Graydon Hoare even stated once that he would have wanted this for Rust:

I wanted & to be a “second-class” parameter-passing mode, not a first-class type, and I still think this is the sweet spot for the feature. In other words I didn’t think you should be able to return & from a function or put it in a structure. I think the cognitive load doesn’t cover the benefits.

Final thoughts

We could ultimately have both mechanisms (with different keywords)! The designs are not really mutually exclusive. This is a very tricky part of language design, but also very important.

Another aspect to consider is that people can easily break the rules of Roto with their registered types. For example, List is passed by value, but the value is an Arc<Mutex<_>> under the hood so it’s effectively passed by reference. Nevertheless, I think that we could establish guidelines about “well-behaved” Roto types.

As always, feedback and suggestions are welcome!