Would Ownership be Easier for Beginners with a Small Change to Syntax?

It seems to me like Rust could be made so much simpler for beginners learning Rust's ownership using only a small syntax change instead of &, we use "lend" when calling the function and "borrow" in the function definition. For function where we are giving the variable to the function and don't need it back we use "give" when calling the function and "take" when writing the function definition.

Even as someone with a few months of experience programming Rust.. this syntax is so much easier for me to read and think about. For backwards compatibility and slightly faster compile times, of course support both.

Thoughts? Would different words that would be better?

Borrowing
Instead of:

calculate_length(s: **&** String) ->usize

AND

fn calculate_length(s: **&** String) -> usize {
    s.len()
}

We instead use:

calculate_length(s: **[lend]** String) ->usize

AND

fn calculate_length(s: **[borrow]** String) -> usize {
    s.len()
}

Giving Ownership
Instead of:

calculate_length(s: String) ->usize

AND

fn calculate_length(s: String) -> usize {
    s.len()
}

We instead use:

calculate_length(s: **[give]** String) ->usize

AND

fn calculate_length(s: **[take]** String) -> usize {
    s.len()
}

Not sure I like surrounding it by [ and ], just an example... what's a better way to use the word there? Perhaps s:take: ?

Could something like this work for lifetimes too (not comfortable enough with them myself)? Anywhere else different syntax could make things easier to understand?

This sounds much like the Learnability vs Efficiency tension in usability (https://www.nngroup.com/articles/usability-101-introduction-to-usability/) or what's known in programming languages as Stroupstrup's Rule (https://www.thefeedbackloop.xyz/stroustrups-rule-and-layering-over-time/). It's also not super obvious to me that the biggest problem here is specifically the syntax, as opposed to the mindset.

One question for you: is [borrow] part of the type? One critical difference between something like ref in C# and &mut in Rust is that the former is a parameter passing mode, not a type (you can't have List<ref int> in C# even though you can have void Foo(ref int x), whereas Vec<&mut i32> works in Rust).

9 Likes

I would like to disagree :slight_smile:
All of the following is obviously very subjective and you might differ, but anyway: I started using rust after using mainly c, c++, java, and Python (all from University, so not realy real world knowledge). Excluding Java, the error messages compilers produced (except python...) were mostly useless.

Switching to rust, the one thing I remember was the feeling of how helpful the error messages from the compiler are. This especially includes things like telling me if I used a value after a move (if some function took a self for example).

Given much better ide integration nowadays I don't see how it would help to see from the call site if a function takes a &self or whatever, because the ide (compiler) will tell me exactly what goes wrong. And if nothing goes wrong, well its rust, so it will behave as expected (looking at you c, c++).

4 Likes

I imagine a lot of confusion could be avoided by simply eliminating method call syntax entirely.

  • No more guessing at how the receiver is passed - use T::method(arg), T::method(&mut arg) or T::method(&arg) instead of arg.method()
  • No more "where does this method come from?" -- it's always obvious whether a method is from a type or a trait
  • No more accidentally calling .into_iter() on a reference and getting the wrong kind of iterator
  • It might cut down on the number of people who try to do "duck typing" like in Python or C++ templates if there literally isn't syntax for it
  • Keep . around only for field access syntax. No more need for the funny parens in (obj.function)()

There may be other advantages, I'll add more as I think of them...

Of course this is not a serious suggestion for Rust, but it's also not entirely meant in jest; I'd like to know how it works in practice. What languages have static typing, traits (or something trait-like), and no method call syntax? (Haskell?)

2 Likes

Haskell is the only example that meets those criteria that I'm familiar with enough to make a meaningful contribution to the discussion. Unfortunately it's also so radically different from Rust in other ways that it's probably not a helpful comparison. In particular, Haskell doesn't have references or mutability in the core language (though there's a zillion ways to emulate them in libraries, e.g. monads), so most of the "magic" that Rust's method call syntax does wouldn't be meaningful in Haskell anyway.


Does anyone here grok Go method call semantics? I know Go method calls will do autoderef of the receiver (so they don't need a separate -> operator), but is that the only "magic" they do?

1 Like

If its more than a few pages of code my workflow always starts with cloning the repository, so I always have my ide :slight_smile:

True

I don't think this is always the case. Given some class hierarchy it isn't obvious at the call site where a method is defined (in which parent class). And being java I haven't seen many projects that did not have some absurd class hierarchies (one project in particular had hierarchies through multiple packages and around 15-20 classes...).
I don't think this is solved perfectly in rust either (if it is possible at all), but at least I know in which crate(s) the code might be (depending on if its a foreign trait etc).

Regarding it being 10 years too late, this could be additional syntax, not replacement.

Additionally, it could be done using something like clippy, where users run a script and it replaces syntax with beginner syntax and/or removes it before compiling, perhaps automatically removing before compiling.

That is just going to confuse people when the error messages they get have no relation to the code they wrote. I generally don't expect beginners will be happy having to learn about this process when their goal is learning to use the language the way experts do. A 'fake' tool is only a justified training device if the costs of using the actual tool are too high. You use a flight simulator because you don't want to crash an airplane. You don't want to use a fake syntax, because a compile error is not a catastrophic failure.

Also, your proposed syntax doesn't really address the meaning of lifetimes/lifetime annotations (where beginners are often confused about them being parameters), nor does it clarify much with respect to generics, where you can do things like pass ownership of a borrow.

2 Likes

The iter() vs iter_mut() vs into_iter() naming convention is used consistently throughout the ecosystem.

Given @H2CO3's answer above, perhaps we all could agree that

3 Likes

It's explicit in the method signature, which also includes return types, generic parameters, and lifetime constraints. All of these things are typically hidden at the call site, so we can't really expect people to understand Rust code unless they have access to the docs or source. Conventions like into_, as_, etc, are there to allow you to have multiple functions with the same purpose without any ambiguity, not necessarily to explain what the code does.

1 Like

Oh the horror!

I have encountered this argument before, and I don't think it's very strong, because:

  • If you don't know a particular function (or any language element serving as an API), you have to consult the docs anyway.
  • If, however, you already know what the function does, you also know whether it consumes or references self.

This applies to both writing and reading code.

2 Likes

One could also use fully qualified syntax to disambiguate the self argument, although this is not considered idiomatic!

// Consumes `vec`
std::iter::IntoIterator::into_iter(vec);

// Borrows `vec`
std::borrow::Borrow::<[_]>::borrow(&vec).iter();

// Also borrows `vec`, but with sugar
&vec.iter();
1 Like

I came into Rust from C++ and it basically had no learning curve at all for me, since it "merely" formalizes/codifies and enforces some pieces of best practice that modern C++ has been advocating for about a decade.

Apparently, our goals differ significantly in this matter. I don't think that the primary aspiration of a language should be to become popular. Relatedly, I'm thinking of Rust as a serious infrastructure language which requires one to know what they are doing. Of course, wider adoption would be great, but in my opinion "it helps beginners" doesn't remotely hit the threshold of adding a completely new kind of syntax (or any feature) to the core language.

I'm a big advocate of education, discoverability, and the continuous improvement of tutorials and the overall documentation. Since the ownership and borrowing model is not an error in the language nor a footgun (in fact, it's the opposite), I don't think that the language is to be blamed and modified if someone didn't understand it. In my view, this is the basic difference between design errors and perhaps unpopular but beneficial features.

9 Likes

Does that mean the 2018 edition changes to the module system worked? :smiling_face_with_three_hearts:


Since we're straying farther from the original suggested change, I may as well say explicitly what I initially thought was such a strong implicit consensus it would've been redundant to state out loud earlier in the thread:

Replacing & with English words like loan does pretty much nothing to teach people how the ownership/borrowing system works. That is such a central and omnipresent part of the language you just need to read The Book before you stand any chance of getting it right (and for the record, every programming language has a core of need-to-grok features like this, even Python). The rules of that system simply do not correspond to any single concept English speakers are already familiar with (unless they've already learned C++ well enough to recognize that Rust has in some sense merely formalized (and simplified) that language's unenforced rules). But once you have read The Book, using loan in some places and borrow in others is also of no additional help.

There are many cases where an English keyword is extremely helpful, and it's quite intentional that modern Rust has far fewer sigils than it used to in pre-1.0 times, but just using English words is not learnability magic. It's only a net win IMO if:

  • the keyword is an accurate enough elevator pitch of what the feature is (e.g. const means "it doesn't change") that you can often get away without knowing the feature in any more detail
  • and there's no ergonomic win from using a sigil because either:
    • the operation is infrequent or specialized enough that a typical user may either never be taught it or forget about it before seeing it again
    • or the operation has such a deep and pervasive impact on the semantics of code around it we do not want to allow someone to "miss" that it's been invoked (which implies we'd never perform this operation without a syntactic marker)
    • or we'd really like a circumfix syntax, for which sigils like parens () and brackets []{} (or sigils plus keywords) typically work much better than e.g. begin and end keywords alone

Unlike const, let, async, try, unsafe, etc., IMO using a word instead of & for borrows meets none of these criteria (e.g. because we do autoref).

8 Likes

The main problem I had with ownership when learning rust was because of lifetime elisions. A signature like fn foo(&Bar) -> &Bar was what I wrote in the code and fn foo<'a>(&'a Bar) -> &'a Bar was what I was faced with when a borrow-checker complained. How lifetimes were elided was not obvious, and I still have a hard time with for<'a> constructs.

Having & mean 'a borrowed reference' was an easy thing to learn, though I do have a C/C++ background, where &foo means something similar - not sure if someone with only Python programming experience would find that as intuitive.

Still, I'd argue that if the main thing that made understanding ownership difficult was the usage of a sigil in place of a keyword, then understanding ownership ceased to be a serious problem, because learning that & means 'borrow' and that data by default is moved from one variable to another rather than copied is easy.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.