I’m sorry for the delay, we’re due to talk about this soon, but it just hasn’t been a priority for a variety of reasons. These are some random personal thoughts, not speaking on behalf of the team:
I think the arguments against “Read + Mut” are compelling - The dual of “read” is “write”, so “read + mut” has always been weird. I think christoph_schlumpf’s point about “Imm” being a contraction of lots of different options is also an issue.
Going with “Immut” seems unfortunate to me though - it is longer than Mut, and you want the immutable thing to be shorter/easier to reach for than the mutable thing. It also is unfortunate that “im-mutable” is negating something instead of being positive. None of these are fatal, but dissatisfying.
Random thought: In arg conventions, you never actually type “read” (we could literally remove it, it serves no purpose other than begin explanatory), and “ref” is the name for parametric mutability. I think this design has worked really well.
What if we align the types to the same nomenclature: “Origin” is read-only, “MutOrigin” is mutable, and “RefOrigin” is parametrically mutable? We’d have to do the same thing with other similar types “RefUnsafePointer” for consistency.
No keyword for immutability is an interesting idea. But Mojo and Python are not immutable-first languages like Haskell or Erlang. List, Set, Optional and many more types are mutable. Renaming those to MutList etc. seems no good option to me.
But I think if the keyword is seldom written out it does not really matter if it is a little bit more than three letters long. Besides being „negative“, another downside of immut is that it is uncommon and quite “ugly”. read on the other hand is not a very good complement to mut and somehow „strange“ to be used in type names like ReadOrigin. ReadonlyOrigin is quite verbose imo.
A common and positiv keyword for something immutable is const. Other options like let and val can’t be used in type names easily. While const does not have exactly the same meaning in different programming languages it is always clear to denote something immutable.
So how about this proposal:
ensure that the keyword for immutable references is the default in all places incl. capture conventions
Rename read to const. It is positive and easy to understand.
Rename all current usages of immut to const: ConstOrigin, Const*Pointer, as_const…
I object to giving up the name “const.” Const and comptime would be great for making the distinction. These can exist at runtime in a section, on the stack, on the heap, or even in a register. The other distinction is simply compile-time computation.
I don’t think this would give up the name “const”. It would complement other potential usages of “const”. Like “var” is used as a “var” declaration and as an argument convention.
Do you want Mojo to become like C++ and overload the name “const” everywhere? I also object to this idea. It would cause a lot of visual confusion and mental complexity.
I don’t think I like RefUnsafePointer, since I would assume that would represent something like a reference to an UnsafePointer or an UnsafePointer to a reference (which I don’t think exists in Mojo). I think that we need to choose whether the mut and read versions of types are treated as specializations or whether some other option (read/var) is the default. I can see arguments for both, especially since ref by default for functions would be very confusing, var by default will cause a lot of moves which isn’t great, and I think mut by default will cause borrow checker headaches as soon as we have lambdas. read by default also has its issues since it doesn’t really match anyone’s expectations (rust is var, c++ is mut) and I keep seeing people surprised by it.
Immutable by default does have some nice properties for compilers as far as not needing to prove not mutations occur to de-duplicate reads. Right now, I don’t think that Mojo has the same noalias for every mutable reference as Rust has due to external origins and the ability to have some shared mutability.
I do like the idea of using const, since that usage of const carries similar meaning as it does in other languages. The only issue I see is that Mojo doesn’t really imply “deep immutability”. You can have a read Span with a mutable origin and still mutate the items inside of the span. This is similar to how C and C++ use const (ex: const**T for a pointer you can’t mutate to one you can mutate to a T), which is probably fine, although Rust is a bit more restrictive in the name of making borrow checking nicer.
I also agree that we need a way to make an immutable variable, and const seems like a good way to do that.
I think ConstOrigin would convey the wrong idea that the value inside is constant. Especially with StaticConstantOrigin which I think should be renamed to ComptimeConstOrigin.
Overall IMO we should reserve the concept of const to mean a constant (unchanging) underlying value. And I still think that the word read conveys all the necessary qualities that the reference gives you (something to read, no promises about stability). And the word write/mut is clear enough (I’m not opposed to either)
This is just an exercise of something to think about big-picture-wise:
I’ve long ago expressed my desire for us to unify and simplify the whole ref vs var concept with just some global ideas about mutability, constant underlying value, and read only “unstable” value.
Making a newer version of the table I posted in the original arg conventions discussion :
Scope would be more or less where in the function body the ref could be destructed, inner would mean that it must be destructed before the end of the scope.
What “kinds” of origins could this translate to?
current keyword & origin name
possible new keyword & origin name
use case
read & ImmutOrigin
read & ReadableOrigin
A reference that can read from potentially mutable memory
mut & MutOrigin
write & WritableOrigin
A reference that can write into mutable memory
out, var & MutOrigin.external
out, var & VarOrigin
For any new memory allocation that the variable owns
Just my opinion, but I don’t see “const” as a contender. As others have said, it’s implications are confusing, and Martin is correct that ‘const’ is simply wrong in this case. The underlying data is not constant, it is just read-only through the origin/pointer/etc.
I agree with you on python, but Mojo is pretty immutable-first, by design:
$ cat test.mojo
fn foo(a: String):
a += "hello"
$ mojo test.mojo
test.mojo:2:4: error: expression must be mutable for in-place operator destination
a += "hello"
^
The confusion may be that first-order local variables default (and can only be) mutable, but that’s not what we’re talking about. Origins and Pointers are for non-local references to data, and the language is immutable first for argument conventions.
I agree with this. Maybe there is another word than Ref?
I agree with this. Read does a good job at conveying the qualities of the reference. I do think it would be an interesting idea to have mut become write so we can have the clear read/write parity. That being said, I do think mut is perfectly okay too.
My personal preference would be: read and ReadOrigin write and WriteOrigin (or mut is fine too)
I think too this would be a viable option. There were some reservations to ReadOrigin in the discussion but I think together with WriteOrigin it would be consistent and easy to get accustomed to. Maybe in some places it has to be named readonly to be more explicit. The current as_immut() would sound strange as_read(). Another reservation to read/write is that it is used used in I/O too (Writer, Writeable trait) which might lead to confusions.
I agree that Mojo is immutable-first concerning argument conventions and for loops. But as long as the default name binding inside functions is var and capture conventions do not default to „immutable“ I still think it is good to have an explicit name for “immutability”.
I understand the issue raised with const although I think from the viewpoint of the function it does not matter if the value behind a const argument convention is „mutable“ outside of the function or not. It is just is not mutable inside the function. That’s the same with the var convention where the value provided can be a transferred value or a copy of a mutable or immutable value. The only thing that matters is that it can always be treated as a var, a locally owned mutable value.
I think “write” would be confusing because it could also imply “write-only.” However, “mut” currently implies “read + write + exclusive,” so it would be inconsistent in the way permissions are handled.
Can’t you see that C++ had to introduce “constexpr” for this useless feature because “const” was already taken? Why waste a keyword on that?
Whether this feature is necessary is debatable, but if it’s introduced, it could simply be an intrinsic function like not_reassigned_or_mutated_in_function_scope.
It’s also debatable what “const” should mean: JavaScript “const” versus C++ “const.”
So you would advocate to have no keyword for an "immutable reference“ argument convention? capture conventions would then need to default to "immutable“ too to avoid the keyword. Would you rename the current ImmutOrigin to Origin and the current Origin to RefOrigin too like @clattner proposed?
The discussion seems to circulate somehow with no clear favorite. One way forward would then be to just contend that there is no absolutely superior option to the status quo and just leave it as it is.
In my naivety I thought renaming read to immut would be an easy quick-fix to improve on the status quo and make it more consistent with minimal effort. Turns out that is not something the community would agree on for several reasons.