Mojo proposal: renaming `read` to `immut`

There are some objections to immut because of its semantics and because it is not a common word/abbreviation (“ugly”).

The proposal with the least deviation from the status quo and the shortest names would then be:

  • read/mut, ReadOrigin/MutOrigin

I like that this proposal keeps the well established mut naming that is not only used as an argument convention but also in inferred refs/origins and parametrized types (mut = true). Implementing this proposal would not break most code while deviating from mut would be quite disruptive.

One minor objection I have to this proposal is that “read” has so many contexts/usages that you can’t simply search for it in the documentation while “immut”/“immutable” is more specific and therefore easier to search for.

Your thoughts?

Hi, all.

Having read through the discussion again, I am in favour of:

In the Mojo documentation the current definition is:

Immutable arguments (read)

The read convention is the default for all arguments. The callee receives an immutable reference to the argument value.

This would become something like:

Readable arguments (read)

The read convention is the default for all arguments. The callee receives a readable reference to the argument value.

Otherwise, if we stick with “immutable” in the documentation then I would prefer immut/ImmutOrigin over read/ReadableOrigin:

Immutable arguments (immut)

The immut convention is the default for all arguments. The callee receives an immutable reference to the argument value.

I see no semantic issues with using immut instead of read:

  • mut provides a ref with Origin[True]
  • immut provides a ref with Origin[False]

“Immutable reference” is a common term to represent “read only” access.

“readable” is fine if it implies “read only” access from the viewpoint of the callee.

See Ownership | Modular

I would prefer `imm` + `mut`.

I remember that `mut` in rust at first felt veeeeeeery awkward to me but you get used to it. Same with `imm` I guess.

Intent

My idea is that it’s not about what it CAN do but more what is SPECIAL about it.

  • `imm`: thing is immutable
  • `mut`: thing is immutable

it clearly conveys the intent.

When it comes to `read` the message is more: “Alright, I can read it. But what about write/mutate?”

Contrast

For me it makes sense to have something like black/white, true/false, mutable/immutable.

Read/Mut feels more like “black/potato” weird :smiley:

3 letters

Also it’s both 3 letters which “feels nice”

1 Like

When you put trait bounds on a type parameter, you list all the behaviours that you need the type to support. If you don’t list something, then you can’t use it. That’s why you can’t mutate an argument or origin for which you did not request mutability.

I agree that traits should be named after what they do, not what they don‘t. But we are discussing an argument convention and not a trait. Additionally the one thing that is special about this convention is that the callee can‘t mutate the argument and not that the argument can be read.

I suspect that whatever this convention is named, it will still be called an „immutable reference“ because that is what is important about it. Therefore I still thinkimmut or imm are viable proposals.

(From a trait perspective: mut = immut & write)

Hi, i like Origin.Readable and Origin.Writable,
the reason is that by having both share the same path,
it is possible to learn the other when only know one !
Dont know for Immut or Mut (have to think),
the main point is just how people can list the options, and how many are there :+1:

1 Like

Here my personal favorites based on what has been discussed so far:

  • immut/mut, ImmutOrigin/MutOrigin

    => a clear distinction: “immutable“ vs. "mutable“

  • read/mut, ReadableOrigin/MutableOrigin

    => states what it does (like a trait): provides a “readable“ reference

  • imm/mut, ImmOrigin/MutOrigin

    => just three letters: imm, mut, ref, out, var, deinit

I for my part would rule out proposals that change mut to write because this would be quite disruptive and beyond the scope of this “clean up” proposal. But I think read/write is viable to consider if such a change is in scope from the viewpoint of the Modular team.

To jump on the bikeshedding, I am in favour of read/mut ReadableOrigin/MutableOrigin

I always disliked mut as a keyword because it looks like the English word mutt and I don’t see the point of the brevity. But in this case read(able) and mut(able) have direct and hopefully obvious parallels.

I also agree with the idea of traits needing to be“positive“ attributes. And in the base case of the trait names, we write things out for clarity. After years of writing Swift I prefer clarity over brevity. We all read much more code than we write after all.

1 Like

As a newcomer here, I just wanted to share my first impressions. I am personally in favor of keeping read and mut as they are, especially since read will be the default. It feels important that the default be something you can understand instantly, without needing to decode an abbreviation.

From what I have seen in Rust and ML style languages like OCaml, bindings and function parameters are immutable by default, and you have to opt in to mutation explicitly. That lines up with Mojo, so I like that the default convention is called read. It is easy to explain in one sentence: you can read, you cannot write. It matches phrases people already know, like “read only reference,” and it pairs nicely with mut as “read vs mutate.”

immut makes sense in terms of consistency with ImmutOrigin and MutOrigin, but to me it reads more like a logical negation of mut than its own clear mode. I would rather see argument conventions read like a small list of distinct modes, each describing what you are allowed to do.

Overall, mut and read already seem pretty easy to understand. Even if read is not a standard keyword across ecosystems, its meaning feels very easy to guess from context.

4 Likes

Just as an objective example:

Given the following code:

fn readonly [o: ImmutOrigin, T: AnyType] (ref [o] val: T) -> ref [o] T:
    return val

If we choose read/mut, ReadableOrigin/MutableOrigin
the code would be changed to:

fn readonly [o: ReadableOrigin, T: AnyType] (ref [o] val: T) -> ref [o] T:
    return val

This composes well, in my opinion

4 Likes

Thank you for this example. Sometimes an example is worth more than many words. It illustrates well the trait-like nature of read/mut when parametrizing over the origins.

Looking again through all what has been discussed including the recent posts there seems to be quite some support and few opposition to this proposal:

  • read/mut, ReadableOrigin/MutableOrigin

It is sound from the viewpoint of the trait-like nature of the read/mut argument conventions. By renaming ImmutOrigin to ReadableOrigin the read argument convention is aligned with its corresponding origin.

I would support this proposal.

1 Like

Does the community and the Modular team agree to this proposal (read/mut, ReadableOrigin/MutableOrigin)?

I am ready to open a pull request:

  • rename ImmutOrigin/MutOrigin to ReadableOrigin/MutableOrigin
  • Add deprecated comptime alias ImmutOrigin/MutOrigin (to be removed in an upcoming release)
  • update the Mojo documentation accordingly
3 Likes

We haven’t fully discussed this internally yet. I think we’d be open to keeping the read/mut argument convention but I would strongly be in favor of not adding the -able suffix to some of these helper aliases. As the -able suffix is mostly reserved for traits and these are not traits - they’re either values or comptime type values.
Simply having ReadOrigin/MutOrigin to maintain parity with the read/mut argument convention.

There are a few things that have still not been looked at yet.
This includes

  • <prefix>UnsafePointer
    • Currently this is ImmutUnsafePointer and MutUnsafePointer
  • the .as_<prefix>() casting function
    • We have as_immut() and as_immutable() inconsistently across the codebase
  • and lastly a few instance of comptime type values such as StringSlice.<prefix>.
    • Currently StringSlice.Mutable and StringSlice.Immutable

These ideally should all follow the same naming scheme for consistency.

In terms of opening a PR - it would likely be a good first step to make a [proposal] before modifying the codebase. So if/when a big-ish change like this is made, we have some documents and justifications to refer back to in a changelog.

1 Like

Hi Nate,
Thank you for the explanation.
I think this is the main point why some in the community advocate against changing read to immut: Argument conventions should be considered trait-like.

If not, then I think we should rename read to immut like I proposed when starting this discussion.

I‘m ready to start writing a proposal but need direction on which way to go with the proposal: The consistent immut way or the consistent read way.

I‘m also fine with just keeping the slightly inconsistent status quo for now (read/mut, ImmutOrigin, MutOrigin).

Maybe the community gets accustomed to all the Immut… prefixed names (Immut*Origin, Immut*Pointer, StringSlice.Immutable, as_immut, …). Then, renaming read to immut becomes the no-brainer easy to implement consistency quick fix I proposed.

Writing a proposal to change read to immut would be easy and implementing it is an almost non-breaking change. I am ready to write this proposal.

Writing a proposal to change all the current Immut… names is much more demanding as immut/immutable is already such a widespread term in the Mojo stdlib and the Mojo documentation. There is not much support for ReadOrigin in the discussion too as read is a word on its own and therefore ReadOrigin is not easily recognizable as „readable origin“ while MutOrigin is recognized as „mutable origin“ because „mut“ is not a word but just an abbreviation. Maybe someone else is ready to write this proposal as I am not totally convinced that it is the way to go and that such a big change is worth it.

One question: Is read the default in unified? So that read values can be omitted in the unified declaration.

unified {read}unified {}

unified {mut a, read b}unified {mut a}

Answer:

there is no default capture convention planned, it is meant to be explicit. We can easily change this and we want to add even more convenience to users through ‘partial’ capture all syntax like {read justOneSpecificVariable, mut *} . Today it is either specify all explicitly or specify one convention for all captures

For now „capture conventions“ do require the read convention to be written out.

With the status quo (read/ImmutOrigin) we will have many read conventions written out in „capture contexts“ and many ImmutOrigin usage throughout the source code.

Any name change to one of those might require a lot of code to be changed in the future.

How to proceed?

Thanks for being patient on this. The holidays have slowed things down a bit, but we should hopefully reach a decision for this sometime this week!

1 Like

I’m curious, what the decision will be…