The case for explicit `read` variable bindings

With the introduction of ref variable bindings (see variable bindings proposal) we gained two new powerful features concerning immutable references:

  • The ability to implicitly declare (deeply) immutable values
  • Binding variables to immutable references

I think it would be a useful enhancement to allow for explicit read value bindings to improve code readability and ease adoption.

Take this example:

import time

fn as_read[T: AnyType](read x: T) -> ref [ImmutableAnyOrigin] T:
    """Helper function to return a value as a immutable reference."""
    return x


fn print_elapsed_time(read value: Int):
    print("elapsed time =", time.perf_counter_ns() - value, "ns")


fn main() raises:
    # immutable value
    ref start_time = as_read(time.perf_counter_ns())
    # start_time *= 2  # error: start_time is immutable

    # deeply immutable value
    ref list = as_read[List[List[Int]]]([[0, 1], [2, 3]])

    # immutable reference binding
    ref list_val = list[0][0]
    # list_val += 1  # error: list_val is immutable
    print(list_val)  # ok

    # sub_list is an immutable reference binding
    for sub_list in list:
        # sub_list[0] += 1  # error: sub_list is immutable
        print(sub_list[0])  # ok

    print_elapsed_time(start_time)  # ok

By introducing explicit read value bindings, the code would be self explaining and the as_read() helper function would not be needed anymore:

import time

fn print_elapsed_time(read value: Int):
    print("elapsed time =", time.perf_counter_ns() - value, "ns")


fn main() raises:
    read start_time = time.perf_counter_ns()
    # start_time *= 2  # error: start_time is immutable

    read list = [[0, 1], [2, 3]]

    read list_val = list[0][0]
    # list_val += 1  # error: list_val is immutable
    print(list_val)  # ok

    for read sub_list in list:
        # sub_list[0] += 1  # error: sub_list is immutable
        print(sub_list[0])  # ok

    print_elapsed_time(start_time)  # ok

Somehow this is just “syntactic sugar” for already available functionality and can be introduced at any time without breaking changes. Nevertheless I think it would be a nice enhancement and fit well with the variable bindings proposal and the current state of the ownership model and argument conventions.

2 Likes

Strongly agree with this :slight_smile:
It improves readability immensely!

Being able to be explicit when needed, is also part of expressivity.

1 Like

Strong +1 from me as well. I would even like mut to be able to be explicitly set as well. I find using ref should be more for cases where the mutability needs to be inferred for generic functions. I think being explicit about the intent is important.

4 Likes

I think it’s also worthwhile to consider people jumping between languages. It’s much easier to interpret read/mut than remember what the default is or what inference rules are. However, I think that we should keep read and mut for references. I’d prefer a const or similar for owned but immutable so that we can keep that consistency.

5 Likes

allowing explicit mut could resolve this inconsistency:

def my_append(my_list: List[Int]):
    my_list.append(0)  
    # Error need to use mut/owned:

def my_append(mut my_list: List[Int]):
    my_list.append(0)  # now it works 

def my_append(ref my_list: List[Int]):
    my_list.append(0)  # error my_list could be immutable since it's parametric


# where as in for loops
def f():
    for my_list in nested:
        my_list.append(0)  # Error use ref/var instead of mut/owned?
    
    for ref my_list in nested:
        my_list.append(0)  # now it works and the ref is mutable

I think if we want to unify arg convention and variable convention we can use:

Convention Read Mutate Move Delete
own :white_check_mark: :white_check_mark: :white_check_mark: :red_question_mark:linear
ref :white_check_mark: :red_question_mark:inferred :cross_mark: :cross_mark:
mut :white_check_mark: :white_check_mark: :cross_mark: :cross_mark:
read :white_check_mark: :cross_mark: :cross_mark: :cross_mark:

I think var comes from the word vary/variable, so it can be easily confused with mut which means mutate - both convey the concept change, so using own instead of var will avoid this confusion.

Both mutable reference and owned variable are mutable/changeable/variable the difference is ownership which the own keyword makes obvious unlike the var keyword.

The case for adding both read - immutable reference and const - immutable owned is to ensure you received a reference and not an owned value, so you would get an error, but I’m not convinced how common it is and if it justify the complexity of adding yet another convention where python has none.
using read for both similar to for loops solves the immutability concerns.

Related: changing the default to read everywhere

1 Like