Chris Lattner, Dec 5, 2023, Status: Accepted, discussion thread
Mojo is still a new language, and is rapidly evolving. We’re learning a lot from other languages, but Mojo poses its own set of tradeoffs that indicate a unique design point.
One of the early decisions made in Mojo's development is that it adopts the
let
and var
design point that Swift uses. This whitepaper argues that we
should switch to a simpler model by jettisoning let
and just retaining var
(and implicit Python-style variable declarations in def
). This has also been
suggested by the community.
Note that immutability and value semantics remain an important part of the Mojo design, this is just about removing "named immutable variables". Immutable references in particular are critical and will be part of a future "lifetimes" feature.
Mojo initially followed the precedent of Swift, which allows a coder to choose
between the let
and var
keyword to determine whether something is locally
modifiable. That said, the design in Mojo 0.6 and earlier has a number of
particularly surprising and unfortunate aspects:
-
The notion of immutable variables is entirely new to Python programmers, and previous experience with Swift shows that this ends up being the very first concept a Swift programmer has to learn. This is unfortunate, because named immutable variables aren't a core programming concept, and not something required to achieve Mojo's goals.
-
The naming of
let
caused a lot of early heat and debate. Other programming languages have a wide range of design points (e.g.const
in C/C++ and Javascript) and there is a divergence of naming for all these things:let
,val
,const
, etc, etc. -
Mojo also has a notion of compile time value (
alias
), which means there are three concepts going around:alias
,let
, andvar
. Most of the uses of (e.g.) Javascriptconst
is better served withalias
thanlet
. -
Both Swift and Rust encourage immutable values - Swift (and currently Mojo) warn about unneeded mutability, Rust makes mutability more verbose (
let mut
), and some propose that Mojo make mutability more verbose. This cuts very hard against a lot of the design center of Python, which doesn’t even have this concept at all: it would be weird to make it the default, but if we don’t, then why bother having it? -
There is no performance benefit to the Swift/Rust design, and I personally haven’t seen any data that shows that there is any safety or readability benefit. If anything, it is the argument when seeing
let x = foo()
that you knowx
will never be reassigned, but any benefit here is small. -
The immutability only applies to the local value, and in the case of reference semantic types (e.g. types like
Pointer
in today's Mojo, but also all classes in tomorrow's Mojo) this is super confusing. We are often asked: “Why do I get a warning that I should change a "var
pointer" tolet
when I clearly mutate what it is pointing to?” -
Mojo does not currently allow
let
’s as struct fields, (onlyvar
’s) which is inconsistent. Swift has a very complex set of rules for how struct fields get initialized that would be nice to not implement for Mojo. There also isn’t a great way to define defaulted field values, e.g.:struct Thing: # This is not actually supported right now, but imagine it were. let field = 42 fn __init__(inout self): self.field = 17 # shouldn't be able to overwrite field?
-
Mojo has a notion of ownership and will eventually have a notion of lifetimes and safe references (including both mutable and immutable references) which will be different from (but can compose with) the
let
vsvar
distinction. It is unfortunate to have different forms of immutability floating around, and we really do need immutable borrows and immutable references.
Speaking subjectively as one of the principal designers of Swift, I will say
that it has several pretty pedantic language features intended to increase
safety (e.g. requiring all potentially-throwing values to be marked with a try
keyword) and many of the decisions were made early without a lot of data to
support them. I believe we should fight hard to keep Mojo easy to learn and
eliminate unnecessary concepts if we can.
The proposal here is straightforward: let’s just eliminate the concept of an immutable named value entirely. This won’t eliminate immutability as a concept from Mojo, but will instead push it into the world of borrowed arguments and immutable references. This would have a number of advantages:
This directly simplifies the conceptual Mojo language model:
- This eliminates one unfamiliar concept that a Python program would have to learn.
- This eliminates confusion between
let
vsalias
directly. - This eliminates a fertile source of keyword bikeshedding.
- This eliminates confusion in workbooks where top level values are mutable
even though they are declared
let
.
This would eliminate a bunch of complexity in the compiler as well:
- Error messages currently have to make sure to say
let
andvar
correctly. - The IR representation needs to track this for later semantic checks.
- This eliminates the need to implement missing features to support
let
’s. - This eliminates the complexity around detecting unmutated
var
s that warn about changing tolet
. - Due to ASAP destruction, CheckLifetimes has extra complexity to reject code
like: “
let x: Int; x = 1; use(x); x = 2; use(x)
” even though the original lifetime of the first “x=1
” naturally ended and “x
” is uninitialized before being assigned to. This has always been a design smell, and it doesn’t work right.
This proposal will not affect runtime performance at all as far as we know.
If this proposal is accepted, I think we should leave var
as-is. Unlike
traditional Python behavior, var
introduces an explicitly declared and
lexically scoped value: we need some introducer and do want scoped
declarations.
The name var
is also less controversial because it clearly stands for
“variable” in a less ambiguous way than using let
to stand for "named
constant". If there is desire to rename var
it would be an orthogonal
discussion to this one and should be kept separate.
If we think this proposal is a good idea, then I think we should stage this to make adoption more smooth and less disruptive. Rolling this out would look like this:
- Build consensus with the Mojo community to get feedback and additional perspective.
- Do the engineering work to validate there is no performance hit etc, and
eliminate the IR representation and behavior for
let
. At this phase we will keep parsing them for compatibility: parse them into the same IR as avar
, but emit a warning “let has been deprecated and will be removed in the next release” with a fixit hint that renames thelet
tovar
. - In a release ~1 month later, change the warning into an error.
- In a release ~1 month later, remove the keyword entirely along with the error message.
We can always keep this around and re-evaluate later. That said, I don’t think anything will change here - the Mojo user community (both external to Modular and internal) has already run into this several times, and this will keep coming up.