Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deep immutability #68

Open
liquidev opened this issue Apr 8, 2022 · 0 comments
Open

Deep immutability #68

liquidev opened this issue Apr 8, 2022 · 0 comments

Comments

@liquidev
Copy link
Member

liquidev commented Apr 8, 2022

Successor to #51.

I've described the rationale behind this model in the blog post, so I'll keep it short. These are the rules we should follow for deep immutability:

mut expressions

mut becomes a qualifier that can be used on any variable or field.

mut_expression := 'mut' '@'? identifier

It's valid both as an assignment target and an expression, described later.

Variable immutability

Variables become immutable by default. Each assignment shadows the old variable.

x = 1
x = 2  # x is shadowed
do
  x = 3  # x is shadowed until `end`
end
assert(x == 2)

To create or shadow an existing variable with one that's mutable, one can use the mut expression with a bare identifier as an assignment target.

mut x = 1
x = 2  # x is NOT shadowed
do
  x = 3
end
assert(x == 3)

mut variables can only be shadowed by other mut variables.

Field immutability

Fields, just like variables, become immutable by default. The mutability is determined in the first constructor. Later constructors inherit mutability from the first constructor.
Since this makes the order of constructor declarations matter, this can (and should) be changed later on in development.
(uses yet unimplemented syntax from #54)

impl struct Example
  func new() constructor
    # @x can only be read from.
    @x = 1
    # @y can be read from and written to.
    mut @y = 2
  end

  func read_x() @x end
  func read_y() @y end

  func write_y(y)
    @y = y
  end

  # This is illegal:
  # func write_x(x)
  #   @x = x
  # end
end

Object immutability

"Object" is a term used in the implementation when referring to any value that requires a heap allocation. The heap allocation is the object.
Values that hold objects are now called references. A reference can be either mutable, or immutable.
Reading a variable or field using a bare identifier (eg. x or @x) decays the mutability of the reference stored inside the variable, except when a bare identifier is the LHS of a dot expression, eg. x.example. In that case, the mutability is preserved as long as the source variable or field is also mutable.
Mutability is also preserved when a variable or field is read using a mut expression.
mut expressions can only be used on variables and fields that are themselves mutable.

self is declared as an immutable variable by default. This can be changed to a mutable variable, by adding the mut keyword after func. A function with an immutable self cannot write to mutable fields, only read from them.

impl struct Example
  func new() constructor
    mut @x = 1
  end

  func mut set_x(x)
    @x = x
  end

  func x() @x end
end

Here's an example to sum things up from the caller's perspective. Lists are used, because they're an object that's easy to instantiate.

# Object creation yields a mutable reference.
x = []
# However, because the source variable is immutable, each read from `x` decays the mutable reference into an immutable one.
# As such, the list inside `x` cannot be modified; only read from.
assert(x.len == 0)  # len/0 does not mutate from the list and as such, is fine to call
# x.push(1)  # push/1 mutates the list and thus cannot be called, because `x` decays to an immutable reference

do
  # Here, we declare x as mutable, such that it's possible for us to delay when it decays into immutable.
  mut x = [1, 2, 3]
  # To make another mutable reference to the same object, the `mut` keyword has to be used.
  # Again, for the reference to not decay immediately we need to store it in a mutable variable.
  mut y = mut x
  # Note that our mutable reference rules are not as strict as Rust's, because we can hold multiple mutable
  # references to a single object.
  x.push(4)
  y.push(5)
  assert(x == y)
  assert(x == [1, 2, 3, 4, 5])
end

Function parameters

One thing I forgot to mention in my post is function parameters. I think they should be declared as immutable by default, with the option of making them mutable explicitly using the mut keyword.

func push_one(mut list)
  list.push(1)
end
mut list = []
push_one(mut list)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: 📦 Backlog
Development

No branches or pull requests

1 participant