Replies: 24 comments 54 replies
-
Starting with a small idea for the bikeshed (sorry)
I would like to add that knowing a value is immutable makes code faster to reason about, reducing the time to comprehend. While it may not be the most valuable information for a compiler, it is quite valuable to the human. This assesment also holds true for the thought of "only preventing accidental mutation". Further, I liked the idea to rename Edit: We could drop |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
@lattner thanks for kicking off proposals in the open. I know it's a pain but I'm happy it's happening that way. I love the idea of separating out the core design from the concrete syntax. I'd have a meta-proposal that I think might be powerful. I'd expect this pattern to be pretty frequent. I'd like to systematically provide a framework for that and always split up the concrete syntax from the abstract syntax. By abstract syntax, I'm imagining a definition similar in spirit to what Rob Harper did in Practical Foundations of Programing Languages (PFPL) though it probably doesn't need to be as rigorous as the Abstract Binding Tree syntax he uses throughout the book. I'm guessing the mojo MLIR dialect is probably a little too specific and is needs to be something slightly higher level and less formal but not as high-level as mojo concrete syntax? That way you could always have concrete syntax discussions in terms of pattern matching how it lowers to abstract syntax. |
Beta Was this translation helpful? Give feedback.
-
Lifetimes proposal looks great,
Having three letters for all of the keywords will allow the user to understand The problem with the proposed removing |
Beta Was this translation helpful? Give feedback.
-
If I am thinking more about Keyword naming and other topics to discuss, it seems clear to me that original syntax has the most sense. Let's consider some example
current:
after change:
after change:
Owning for me needs special treatment, because during declaration of owned argument we must be very explicit to not confuse others. |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
I suspect this will be downvoted, but we have another closing character set for declaring lifetime parameters, eh: So it's like a lifetime closure. Also most likely library developers will mostly encounter the side effects so most other developers might seldom have the need to use {} . Anyway, interested to see if I'm chased out from the shed due to introducing {} to the python world. |
Beta Was this translation helpful? Give feedback.
-
The lifetime proposal seems well thought out. On the keyword renaming discussion:
|
Beta Was this translation helpful? Give feedback.
-
w.r.t lifetime of self, it's not clear to me the different between
vs
|
Beta Was this translation helpful? Give feedback.
-
Here is a crazy idea:
IMHO lifetimes can be described as "colourations" of the reference semantics. As such it is important to see how many colours are there and which reference belong to the same colour. Therefore a numeric identification of a life is more useful. That does not mean we should not name the colours though. I propose to have an option to associate a lifetime with name by postfixing the numeric value with Here is a more complex example taken from this blogpost where the author advocates for giving the lifetimes in Rust more meaningful names then just
With numeric identification of lifetimes it would be sensible to suggest The example from Provenance tracking and Lifetime in Mojo would adopt as following:
I think the more exotic features from Rust listed in Provenance tracking and Lifetime in Mojo, will also feel more intuitive with a numeric identification of lifetimes, where a smaller number outlives the larger one and we can identify equality by something like this:
Where we signify that Motivation behind this rather radical proposal is following: Generic Lifetime Annotations is a concept which is not familiar to most of the potential users of Mojo. Making it look similar to other concepts like generic types and compile time arguments might cause more confusion as user unfamiliar with the concept will not be able to identify something "unfamiliar" from the function or struct signature. So IMHO following signature:
might suggest to the user, who is unfamiliar with the concept, that Second argument for the design I came up with was based on my experience, giving meaningful names to lifetimes is hard and almost nobody does it, this is why most of the Rust code uses |
Beta Was this translation helpful? Give feedback.
-
Hey @lattner, sorry, I've not had much time to put all my thoughts in a coherent form yet. I wanted to at least put a few things are your radar that make my spidey senses tingle when I read through some of this. 1) Awareness of RustBelt's contributions First, I want to make sure you're aware of the awesome work by Derek Dreyer and team (a former Bob Harper / CMU PhD now at MPI) has done as part of the RustBelt. He was able to formally verify rust's borrow checker correctness and safety including unsafe. I think it's at least worth being aware of and noting what information he needed to keep track of when reasoning about proving the correctness of unsafe code (what the main paper calls Interior Mutability). The approach he used of creating borrow predicates and lifetime tokens (specifically the data he needs to track to reason about those programs seems like it should also be useful in implementing lifetimes and ref counting (especially across async boundaries). RustBelt defines the following predicates and the corresponding parameters required (e.g. a concept of a thread id) to reason about the lifetimes in unsafe (code with Internal Mutability ie pointer manipulation) Derek tends to be friendly and respond to strangers reaching out and asking questions :) 2) concerns about lifetimes as type parameters I'm not able to fully articulate this one yet but I worry this isn't the right direction to go. 2.1) I think this might prove to be a stumbling block once you try to figure out traits and function overriding and how the sub-typing relationship plays with restrictions or widening of the lifetime. It's worth being aware of some of the issues Rust had to resolve on that front: 2.2) It's not clear to me how this would work with partial type specialization. e.g. I might want to create a type alias that partially specifies some of the type params but not the lifetime. Does this open us up to needing to handle named parameters here? 2.3) This is maybe the crux of my thoughts (that I don't have a crisp articulation of yet). I think there's a flavor of the expression problem at play here. The lifetime parameter(s) are making a statement about the lhs binding to a value of the type. I think this is very similar to how linear type systems need to track usage counts. QTT (refinement of McBride's original proposal and the Idris 2 (Edwin Bradey) solved this expression problem by keeping these 2 parameters separate and realizing a (usage-count, Tau) pair of parameters at the binding site. (note I'm not suggesting we expose a linear type system but that lifetime and arc implementations have similar concerns to implementing linear types since you have to track usage).
|
Beta Was this translation helpful? Give feedback.
-
Perhaps an unpopular opinion, but to me the original syntax with "&" was more intuitive. |
Beta Was this translation helpful? Give feedback.
-
The initial document notes that relationships/constraints between lifetimes as a future area of interest rather than a present one (ala The Rustonomicon section on variance notes that this is a common source of confusion:
When you get up to three lifetimes, for example |
Beta Was this translation helpful? Give feedback.
-
Variants for all the keywords (Lifetime): ## 1) ref(=default)
fn add(x: Int, y: Int) -> Int:
return x + y
fn add(mut x: Int, mut y: Int) -> Int:
x += 1
y += 1
return x + y
fn set_fire(mutown text: String) -> String:
text += "🔥"
return text
## 2) own(=default)
fn add(ref x: Int, ref y: Int) -> Int:
return x + y
fn add(mutref x: Int, mutref y: Int) -> Int:
x += 1
y += 1
return x + y
fn set_fire(mut text: String) -> String:
text += "🔥"
return text
## or
fn add(in x: Int, in y: Int) -> Int:
return x + y
fn add(inout x: Int, inout y: Int) -> Int:
x += 1
y += 1
return x + y
fn set_fire(mut text: String) -> String:
text += "🔥"
return text
The last variant (ref as in / inout) seems to be better readable. |
Beta Was this translation helpful? Give feedback.
-
I have a scheme in mind that maybe could minimize the keywords and at same time keep their meanings consistent. It consist of:
Bellow we have a clue of how these keywords could interate: const a = "abc" # a: imutable owned
const b = ref a # b: imutable reference to "a"
var c = "cde" # c: mutable owned
var d = ref c # d: mutable reference to "c"
const e = ref c # e: imutable reference to "c" Bellow as "const" is the default, we don't need specify it: def do_something(ref a: String, var ref b: String, c: String, var d: String)
# a: imutable reference
# b: mutable reference
# c: imutable owned
# d: mutable owned
... In the end, we would have only these 3 keywords: |
Beta Was this translation helpful? Give feedback.
-
Semantically I like the keywords as each one is a noun, so when speaking about the code it makes sense. |
Beta Was this translation helpful? Give feedback.
-
Update Jan 2024: Since making the below post, I've iterated a lot on the design. I plan to publish a major revision at some point. There are a lot of subtleties that make the design presented below untenable. In particular, storing references inside structs is very tricky, and I no longer believe the naive syntax presented below is appropriate. @lattner Here is some very late feedback on the lifetimes proposal. I meant to provide this feedback months ago, but I wanted to investigate these ideas more thoroughly. Unfortunately, I haven't had the time to do that yet. So I'll just convey the gist of what I was thinking, in the hope that it might be helpful. In short, we might be able to express provenance as follows:
As well as being more succinct, this syntax might facilitate a more general semantics than Rust, by allowing us to abstract over the mutability of references. The basic ideaI'm exploring a syntax for lifetimes that is much simpler than the proposed Part 1: Returning referencesLet's look at an example. For the sake of comparison, here is the syntax of the current proposal:
Here is an alternative syntax that seems promising to me:
Above, the return type One nice thing about this syntax (apart from being more concise) is that it communicates an important aspect of the function's behaviour. If you didn't otherwise know what the function did (imagine it was called Here's another example:
This signature specifies that the return value will be a list which is populated with references to the objects that With the proposed syntax, it would also be possible to return references to non-local variables:
This means that we wouldn't need a This also gives us a very cool and dead obvious syntax for methods that return references to
It would also be possible to return references to public struct fields, and computed properties:
To return a reference to a private struct field, I suspect you could just expose it via a computed property, and use that as the return type. Referring to collections would be slightly more challenging, because the elements of a collection cannot be named by a set of variables, or a set of paths. Indeed, the elements can't be enumerated at compile-time, because there are dynamically-many elements. So instead of referring to collection elements by their name, we might be able to refer to them by the type parameter that defines them:
Here, I'm assuming that the type parameter associated with the Polymorphism over mutabilityOne cool benefit of this syntax is that it allows us to abstract over the mutability of the reference being returned. Consider this function from earlier:
Given this signature, it seems reasonable that if the strings used as arguments to a particular invocation of this function were mutable, then the reference returned should also be mutable. Basically, the type signature should be read as "I don't care if In contrast, the
This suggests that if the function implementer wanted to give the caller the freedom to mutate the result, they would need to offer a second version of the function:
This is a very real nuisance that Rust programmers often encounter. It forces many APIs and traits to be duplicated twice. (As an example, see Index and IndexMut.) Abstracting over mutability technically doesn't require the syntax I've proposed, but IMO, the syntax makes it apparent that a function does not have the authority to specify the mutability of the references it returns. Compatibility with access modifiersIt's worth thinking about how this syntax might work alongside access modifiers—on the assumption that Mojo might eventually offer them. Consider a scenario where an opaque data type that has no type parameters (such as a
What constitutes a suitable solution will depend on Mojo's approach to access control and "privacy", which is not something that has been decided yet. Regardless, this is an important problem to solve. I have some thoughts on how to solve it, but nothing worth sharing yet. Part 2: Storing references in argumentsSo far, I've presented a syntax for describing the provenance of return values. But when a function has mutable arguments, there is also the possibility that references are inserted into those. We need to extend the proposed syntax such that it can describe the provenance of such references. It might look something like this:
This syntax would combine well with the
This aspect of the syntax needs more attention, but I haven't been able to dedicate the time yet. The syntax really needs to be co-designed with the semantics for stored references, which hasn't been finalized. The Mojo team have been putting a lot of work into the semantics, so I'll leave it to them to figure out how this syntax might (or might not) integrate with it. SummaryAt the very least, I think the proposed syntax is really interesting, and worth further investigation. The core question is: is it possible to use variable names, rather than lifetime parameters, to describe the provenance of references? If it were possible, it might greatly simplify the syntax for lifetimes in Mojo, and ultimately make Mojo a simpler and more beautiful language. |
Beta Was this translation helpful? Give feedback.
-
How about sth like that?
(
FAQ: Q: Why to replace |
Beta Was this translation helpful? Give feedback.
-
Consider using the following proposal for 4-character keywords:
This approach ensures conceptual consistency, where all mutables start with And with this approach Regardless of whether this exact naming schema appeals or not, please consider prioritizing consistent naming conventions. Using abbreviations like |
Beta Was this translation helpful? Give feedback.
-
^ => move let a: String = "Hello Chris" print(a) #Error |
Beta Was this translation helpful? Give feedback.
-
Another bikeshedding opinion: ref / constref in replacement of mutref / ref The syntax 'mutref' creates a cacophony in my mind and I think its because 'mutt' sounds unpleasant. Considering this syntax will be used a lot and this is the time for exploration, ideally we end up with something smoother sounding. I'm hopeful of Mojo not just as a useful and fast language, but also as a beautiful and intuitive extension of Python. Id even prefer deltaref over mutref |
Beta Was this translation helpful? Give feedback.
-
Beautiful. We talked about syntax for life at Mojo 🔥 and some of you gave great, mildly amazing suggestions, while others weren't. We talked about the importance of age in mojo 🔥 and its effective role in transferring the language to another world, but let us leave all that and ask the most important question: When will it be completed? Lifetime support in Mojo programming language 🔥 This is what we should focus on. Any additions to the syntax can be talked about later. |
Beta Was this translation helpful? Give feedback.
-
Another really interesting design decision is OCaml's modes, which decouple lifetimes and types, therefore reducing complexity:
I don't know how well this can work in Mojo:
There's also Project Verona by Microsoft Research, inspired by Rust, Cyclone and Pony, that aims to answer these questions:
See this talk for more info. Edit: Also check out Antelang: |
Beta Was this translation helpful? Give feedback.
-
Hey, saw this linked in an HN comment and it nerd-sniped me! I know this might be outdated, but here's some thoughts anyway:
Something that helped simplify implementation for Vale was to universally add an implicit "self region" generic parameter. In Mojo speak:
(this is under the hood, user needn't see It also gave the use-site a convenient place to put extra region/lifetime bounds; the compiler could lower an e.g. Biggest downside was that it was a "special" generic parameter, and therefore a design smell... on the other hand, once I took it further and considered having multiple "implicit self region" generic parameters, some truly weird things happened which later solved some holes in Vale's design, ironically. Might or might not be applicable to Mojo (Vale's region borrowing is rather unusual) but could be worth exploring.
I was exploring this with Jon Goodwin (of Cone) for his language so I agree with your hope/expectation, and there are also a few other mechanisms that allow more aliasing+mutability along those lines. Happy to explain more if anyone's curious. Random thought re: these "transfer functions": when I implemented Vale's logic that mapped callee lifetimes to caller lifetimes, I felt some eerie echoes with how linear algebra uses matrices to transform vectors from one coordinate space into another. If anyone's good at graphics and compilers, I'd be curious to see how far that echo reaches. |
Beta Was this translation helpful? Give feedback.
-
I really wonder if Rust like lifetimes is a proper fit for Mojo. Mojo seems to be targeting programmers who are using Python and that for convenience, they want things done as easily as possible and don't want to deal with memory management. What happens when you present a Python programmer with Rust like lifetimes? I bet they will hiss and spit. People who like Rust will continue to use Rust. Many languages seems to be jumping on a Rust like single ownership model today but that only solves half of the problem. There is still multiple ownership, like classes in Python which you can toss around without thinking about it. Is there another way using inference instead, that the compiler can decide what is borrow and what is not? If you look at the language Lobster, it uses static analysis to reduce the number of reference counts. |
Beta Was this translation helpful? Give feedback.
-
Hi all, I put together a draft of the lifetimes proposal, split between the new capabilities in one doc and then a syntax bikeshed proposal in the second. I'd love thoughts and feedback on this thread:
Provenance Tracking and Lifetimes in Mojo
Keyword naming and other topics to discuss
Beta Was this translation helpful? Give feedback.
All reactions