And to me it makes sense that we use related words for memory traversal in IO. If it’s possible in the parser to allow that function names be the same as the argument conventions like read and write, I think we should keep them.
One big advantage of the current ImmutOrigin/MutOrigin namings is that they are easily recognizable as related and unique to value access. immut and mut are not used in other contexts in Mojo. as_immut() or ImmutPointer can easily be recognized as value access related. I would rather stay with those than change them to something more ambiguous like ReadOrigin/as_read()/ReadPointer even if the argument convention stays read (sigh).
The words „immutable“ and “mutable” are ubiquitous throughout the Mojo documention and in discussions related to value access.
This would be my argument for staying with the status quo and not do a big refactor to something else that has its tradeoffs too.
I have followed this thread with great interest and am fascinated by how much consideration goes into naming things. Personally, I really like the simplicity of read/ReadOrigin and write/WriteOrigin, as it is easy to infer what can be done with the reference. I agree that read and write could be confused with other concepts from IO. Therefore, I would like to suggest the following:
view and ViewOrigin
edit and EditOrigin
The idea of viewing vs editing mode is already widely used in consumer software like Google Docs or Microsoft Word. This means that most people who are somewhat familiar with software should know what these terms mean. I also like that editing implies reading + writing, as you are not simply writing something new but reading what is there and changing it. The words view and edit are also short and do not have to be abbreviated to be used as keywords, which is always a plus.
I am looking forward to any feedback you might have
Yes, naming is quite a difficult thing to get right, and there are many trade-offs. edit and view were considered in this (lengthy) issue too: https://github.com/modular/modular/issues/3623
AFAIK the issue with edit and view was that they are more common in document/file handling APIs than in value access. “immutable reference” or “read-only reference” are quite self-explaining expressions. “viewable reference” or “view-only reference” are somehow less common.
IMO a good name/abbreviation is:
expressive in representing the intended semantics/meaning/idea/concept
easy to use in spoken or written sentences
familiar
unambiguous
short
consistent with other names in the same context and the programming language as a whole
applicable in all relevant language constructs
In the end someone (Modular) has to decide. I still think the status quo is good enough because all other discussed alternatives have similar or bigger trade-offs too.
„ Hi Christoph, we’re still (slowly) debating the “read” keyword and related naming topics. We procrastinated on it a bit because this isn’t the most fun topic to debate. There isn’t a perfect answer, but regardless we need an answer and want to converge this topic to lock things in. Stay tuned for an update from the team in the next couple weeks (at the latest).“
I started writing a PR to remove a few arbritrary usages of the read keyword in argument conventions (e.g. in LinkedList) based on a discussion on Discord . I intended to add a rule in the stdlib style-guide to not use the read keyword in argument convention as it is the default.
“Just remove the read keyword from argument conventions.”
Function arguments are immutable (read-only) by default. There is no need for an explicit read argument convention.
This solution would have the following advantages:
Keeps the source code concise and consistent across the ecosystem. No need to define a style-guide (linting) rule on whether and where to use a read keyword in argument conventions. >99.9% of the stdlib source code does not use the read keyword in argument conventions.
Avoids confusion that might occur when the default and explicit read argument conventions appear in the source code side by side. Q: “Are there subtle differences between them?” A: “No, there are no differences at all.”
Aligns well with the for loop value bindings that are immutable by default and do not allow a read keyword: for i in range(10): # i is immutable
No need to agree on a keyword for the default argument convention anymore. The different *Origin type aliases do not need to be renamed immediately as they are “just” aliases and ImmutOrigin does not have any friction with the default argument convention.
Implementing this solution would be quite easy as the explicit read argument conventions are only used in very few places. The read keyword would just have to be removed there.
Additionally, as a consequence, I propose to remove read from unified closure conventions too and make immutability the default: unified {read} → unified {} unified {read a, mut b} → unified {a, mut b}
This would align argument conventions and closure conventions and remove the read keyword entirely from the source code.
IMO there is no actual problem to be solved here. I don’t understand why this thread even exists?
If read turns out to be an unhelpful keyword I’m sure it will be removed in a few years, once Mojo’s syntax has stabilized. Until then, I think it’s too early to take any action. Certain corners of Mojo’s syntax are yet to be designed, and once that happens, it’s possible read will have a purpose.
I think there should be something. When moving between languages, it’s nice to not need to remember the default and instead be able to explicitly spell things out. You can also made the LSP show a phantom read as a setting, which can help people who have to jump between languages a lot.
Hover hints from the LSP make it clear if an argument is mutable, but not so for a variable. Assuming no explicit ‘read’ keyword as proposed by Christoph and referring to Owen’s comment about people new to Mojo, perhaps mutability should be explicitly shown by the LSP? See the code snippets below for examples. Hover Hints - Current
fn foo(bar: Int, mut start_time: Int):
pass
# (argument) bar: Int
# (argument) mut start_time: Int
fn write_to(self, mut writer: Some[Writer]):
pass
# (field) self.bits: UInt32
# (argument) mut writer: T
num_workers = parallelism_level()
for thread_idx in range(num_workers):
pass
# (variable) var thread_idx: Int
# (variable) var num_workers: Int
Hover Hints - Proposed
fn foo(bar: Int, mut start_time: Int):
pass
# (argument) imm bar: Int
# (argument) mut start_time: Int
fn write_to(self, mut writer: Some[Writer]):
pass
# (field) imm self.bits: UInt32
# (argument) mut writer: T
num_workers = parallelism_level()
for thread_idx in range(num_workers):
pass
# (variable) imm var thread_idx: Int
# (variable) mut var num_workers: Int
Another (divergent) thought about variable assignments. Given that at runtime a variable is mutable by default, would it be useful for the programmer to have an ‘immutable’ keyword to indicate that a variable’s value cannot be modified after it is first assigned? The compiler would use the imm keyword to check for modification in any code where the variable is in scope. Example 1:imm num_workers = parallelism_level() or Example 2:imm var num_workers = parallelism_level(). Example 3:
struct foo():
var bar: Int
var start_time: Int
fn __init__(out self, bar: Int):
self.bar = bar
imm self.start_time = time.perf_counter_ns()
Just to follow up on this, the design team is still discussing this. The “read” arg convention is effectively never written, so we don’t consider that to be the priority. Instead we’re prioritizing the discussion around the type prefixes for things like ImmutOrigin and ImmutUnsafePointer.
We discussed multiple different options, including things like ImmUnsafePointer etc, but would like to explore aligning these aliases with argument conventions. It would be wonderful if we could just make the ‘mut’ parameter of these types have a default value of False. This would allow things like:
fn foo(a: UnsafePointer[Int]):
to default to immutable. Other sorts of mutability would require more explicit syntax, e.g.:
for mutable or parametric mutability respectively.
We don’t know how well this will work out, so need to do some prototyping to explore this. Once we determine if this approach can work, we can return to evaluating the argument convention.
"Respectfully, I think you’re over-optimizing for character count here. We’ve already been discussing immut as the logical partner to mut.
Using imm just to satisfy a ‘3-letter rule’ feels like a step backward for readability, especially when the language is already moving toward ImmutOrigin internally. Plus, your post says both imm and mut mean ‘immutable’—which is a bit of a hallucination! Let’s stick to immut; it’s clearer, matches the community direction, and avoids the ‘black/potato’ pairing you’re worried about without losing the meaning of the code."
Actually ‘imm’ feels simple but their is no much difference with a 5 letter ‘immut’
I‘m very interested in what the design team decides concerning the naming of types with parametric mutability.
Pointer[mut=_], MutPointer, Pointer
vs. Pointer, MutPointer, ImmutPointer
I‘m so accustomed to Immut… that it feels quite normal to me. Maybe it‘s because any value of a type that doesn’t have a mutability prefix (Mut… or Immut…) has an inferred mutability based on it‘s origin (e.g. List, Int, …). Therefore I expect Pointer to have a reference with inferred mutability to the value it is pointing to and ImmutPointer to have an immutable reference to the value.
This violates my expectations of the naming convention usually used with polymorphism,
where the base name is the generic type, and the prefix/suffix names are the specialized types.
We spend more time reading code than writing code, therefore
I think it is more important to optimize for code readability than for code writability.
Code is read most often in function signatures, therefore I think optimizing for readability of function signatures is the most important.
When looking at it in function signatures, from the most generic type to the most specific type:
Arg Type
Accepts
Returns
UnsafePointer
immut, mut
immut, mut
ImmutUnsafePointer
immut, mut
immut
MutUnsafePointer
mut
immut, mut
Regarding name readability, my rule of thumb is:
For rarely used names (like uncommon types or variable names local to one function), longer, more descriptive names are better for readability.
For frequently used names (like common types or keywords), shorter names or abbreviations remain readable because they are familiar, and they help keep line lengths shorter. This is especially important in function signatures, which can grow long quickly.
Parametric mutability should be the default for pointers. It should be obvious why this is the case so I will say no more.
I agree that UnsafePointer should probably force the user to choose or be parametric. I think that having it be immutable by default will lead to a lot more origin casts compared to being parametric. If someone means ImmutUnsafePointer, I think they can use that alias.