Hacker News new | ask | show | jobs
by bjarneh 1119 days ago
> I'll explain what the move keyword does later in the article. For now just trust me that it is needed for the code to compile.

That does not take a lot of trust, after a few rounds with that compiler. At least error messages are good with the Rust compiler. I've got very limited experience with Rust, but it does seem like a language with a massive threshold for beginners.

4 comments

I know I keep repeating this, but...

Rust does not have a massive threshold. Memory management has a massive threshold. Rust just has a lot of safety rails you might keep bumping into if you struggle with memory management. The fact that other languages let you drive off the cliff doesn't mean they have a smaller threshold.

It has a massive threshold for beginners for sure.

Personally I have a background in C/C++ and moved towards C#, Go, JS etc. With an interest in many more, but the above were my main languages for a decennia.

9 years ago I picked up Go, and have used it the most as my main language since then. 7 Years ago I picked up Rust, but only since 6 months ago really intensive (including publishing the guide that came out of my experience at https://rust-lang.guide/).

Over those 7 years I've only done a production project around the start of that journey and now since 6 months ago I'm writing production code in it again and haven't stopped since. But I've learned the language like 3 different times in that journey.

It's not an easy one to get into. A lot of things to wrap your head around, especially if your goal is to really master it, rather then just "woohoo I can compile my program". I still not master it, but I do feel now fluent in it and can express my ideas well. There are also no more fights with the language or its tooling.

Compared to that, Go(lang) is super easy to get into. Ridiculously easy. Perhaps too easy. You can give it to a programmer of any level, and they probably can ship their first feature that same day. The difference is however, that. First of all with Golang it's super easy to ship code with nil pointer exceptions in it or data races. I've seen it in the best code bases. And sure, plenty of people will tell if you do it "right" you'll have no issues. I've hear similar comments from ex-colleagues still in the C++ world as well. Secondly, Golang is very opinionated and if you derive in ideas or needs a bit from what they want you to do, you are in a bit of a problem. This has been slightly improved, but it's still very limited and still a lot of magic that only built-in features can perform. And thirdly, the language is very minmal (Golang). So codebases look very verbose very quickly. Also harder to express ideas (as error support is limited, and sum types are not a thing, neither are other things like pattern matchings). It's also a lot less "functional".

This is not to compare Golang vs Rust too much, even though I did. My point was more, and I probably butcher my own comment not making that point well, is that I think it's fair to have a massive threshold for beginners to get into a programming language. As long as in return it means you get a very powerful tool in rerturn to do some of your work. And Rust is def. such a tool. Many people boast about how peformant it is. And while that is true, it is for me for most projects more of an extra benefit rather than the things I really like it for. What I like with Rust is that it gives me really high confidence in my code (comparable to projects I've written in Elm and Haskell in the past). When my project compiles I really am pretty confident it will not give me runtime surprises beyond mistakes elsewhere (e.g. infrastructure choices). It's a very expressive language and pleasant to use in that way.

And then there is of course the fact that it is truly FOSS, has a very nice community, great documentation and a lot of excellent learning resources.

I am not a fanboy of much, neither of Rust, but I do really appreciate its existence and I am happy to use it where it suits. Yes it is a massive threshold, but one worth to pay.

"And sure, plenty of people will tell if you do it "right" you'll have no issues."

My answer to that is always that if you are able to do it right in C or C++ (and maybe golang) you should not run into any issues with Rust. Especially you should not ever have to fight to borrow checker because it does the job you should be doing as a C/C++ programmer in your head anyway.

The borrow checker rejects many otherwise valid programs. So if you do your job in correctly in C/C++, the borrow checker might accept the Rust equivalent or it might not.
Maybe that was true in 2017, but today borrow checker and compiler in general has covered so much of the Rust design space that the "correct programs" it rejects are more like rejecting Duff's Device kind of code. You are more likely to find yourself implementing low-level data structure or device interface where you need raw pointers tightly localized in the unsafe{} scope.
It rejects anything it can't prove. Sometimes that proof is trivial for the programmer. Consider a string table. You intern by passing an owned String, the string table sticks it in a HashMap and gives you back a &str with a lifetime matching the table. From a C/C++ perspective, that's fine; Rust doesn't know that you'll never be removing things from that table, but you certainly can (and encapsulation can help you enforce that).

Since 2017 it certainly admits quite a few more programs, but it's still pretty easy to find things that require a different approach than they would in C or C++.

The article says that this code doesn't compile in 2023. Assuming the intention was to print "fox", I don't see how it is incorrect:

  let mut animal = "fox".to_string();
  let mut capture_animal = || {
      animal.push_str("es");
  };
  //ERROR:cannot borrow `animal` as immutable because it is also borrowed as mutable
  println!("Outside closure: {animal}");
  capture_animal();
> Especially you should not ever have to fight to borrow checker because it does the job you should be doing as a C/C++ programmer in your head anyway.

It adds mental overhead.

I don't even like the "const" qualifier for functions in C++. It is so hard to get a design right upfront when thinking about non trivial 'const'-chains.

> It adds mental overhead.

The borrow checker removes the mental overhead of borrow-checking. You don't have to do it by yourself, you can lean on the borrow-checker to do that for you. In C++ you're all by yourself. You may pretend you don't need to do it, but you will run into troubles pretty fast.

Moving stuff into a closure is a perfect example of that. Try to capture an arcmutex to a bunch of threads in a closure? The compiler will note that arc can't be copied and to try moving it. It will then say you are trying to move the same arc to multiple threads, so you clone the arc. And if you try to just use an Rc it will tell you that can't be sent between threads (not thread safe) use arc instead. Those messages can absolutely guide you a long way before you realize your whole concept won't work, and that is frustrating, but those steps usually make you understand the whole issue.
"It adds mental overhead."

To me `const` removes mental overhead -- if I pass a const object somewhere, I can be sure it doesn't get changed. I don't need to inspect the code to make sure it doesn't call any methods that modify the object or trust a probably outdated comment or documentation.

When you are using the function, ye. Not when you are writing it. And if you someday want to change a const method to non-const you could have a cascading constness change all over the place.
But usually functions are used more than once.

"cascading constness change"

Which is good because invoking code may rely on function being read-only.

>"...it does the job you should be doing as a C/C++ programmer in your head anyway."

False. Too many false positives

"False" seems too strong. The job it does is track lifetimes. That's a job you should be doing anyway in C/C++.

It does so better than you can in that it doesn't have false negatives anywhere you're sufficiently playing by its rules.

You can also do so better than it can, in that you can see reasons things are okay that it can't understand.

I agree that there are too many false positives to assert that a competent C or C++ programmer doesn't have learning to do to appease (and leverage) the borrow checker.

> Compared to that, Go(lang) is super easy to get into. Ridiculously easy.

A couple weeks ago I tried to get started with Go and the hello world code example wouldn't work with `go run` with some obscure module error that's meaningless to a noob, but sure it's "ridiculously easy"

Thanks for your insight.
> And sure, plenty of people will tell if you do it "right" you'll have no issues.

Go is inspired a lot by CSP, and allows you to apply CSP calculus when using goroutines and channels.

It doesn't make it impossible to make mistakes, but it does give you tools needed to reason about your concurrent program, which is more than can be said of most other languages' concurrency support.

And yet goroutines share the same address space, don’t have a good way for the parent to wait for them and even panic silently.

There’s a reason many projects ban the go keyword in favour of a wrapper that fixes some of this.

Go requires constant vigilance, sadly.

Yes, as I said, Go doesn't make it impossible to make mistakes. It doesn't hold your hand and slap you when you go off-road.

But saying it "requires constant vigilance" is an overstatement. As long as you put a little bit of thought in the concurrent code you're writing, it's very easy to do things right. Data races are easy to avoid if you only transfer ownership by sending pointers through channels.

It would’ve been trivial to make the language disallow nil, separate goroutine address spaces, crash the process on unhandled panic in any goroutine, etc.

The reason Go annoys people is the unforced errors. Sure, it gets a lot right. But what it gets wrong had solutions long before Go existed and those solutions were wilfully ignored.

> It would’ve been trivial to make the language disallow nil

Which would interfere with Go's philosophy that zero should be a valid (usable) state for types. What would be the default value of a reference type without nil?

> separate goroutine address spaces

This is not trivial to implement. The only way I can think of is to start each goroutine in a new process, which brings along other downsides - everything must be copied, cannot pass file/socket handlers between goroutines, etc. Doesn't seem to be worth it.

> crash the process on unhandled panic in any goroutine

Go does crash the process on unhandled panic in any goroutine:

    # main.go
    package main

    func main() {
        go func() {
            panic("hehe")
        }()

        for {
        }
    }

    # go run main.go
    panic: hehe

    goroutine 5 [running]:
    main.main.func1()
            /[...]/main.go:5 +0x27
    created by main.main
            /[...]/main.go:4 +0x25
    exit status 2
> The reason Go annoys people is the unforced errors.

This is the criticism I've also had, and raised an issue on GitHub. Unfortunately, it's impossible to fix this without breaking backwards compatibility.

> But what it gets wrong had solutions long before Go existed and those solutions were wilfully ignored.

They were wilfully ignored, but only because the tradeoffs didn't seem worth it at the moment. You make it sound bad, as if Go devs were simply high on crack and knew what they doing was wrong, but did it anyways, when in reality it's more of a case of those "solutions" not being compatible with the goals of the language, or simply nobody coming up with an elegant way to incorporate it.

Can’t the parent wait by simply using the waitgroup sync functionality? That’s what I’ve been doing in my greenfield Go project at work.
Sure, but you can neglect to do it or get it wrong. The language could expose primitives that don’t allow it instead.

In practice, where I work we disallow the go keyword in review and require use of a wrapper that takes in a closure and also is a wait group. It’s much harder to get wrong.

> it does seem like a language with a massive threshold for beginners.

Programming with manual memory management should be (comparatively) hard. It's not where beginners should start. C makes it far too easy to write code with objectively bad effects on the system, as evidenced by the countless vulnerabilities discovered in critical real-world systems over the last half-century.

> after a few rounds with that compiler.

I find this phrasing curious. Do you feel that you are fighting the compiler?

You will probably get a lot of varied responses and also some people who just defend the language of their choice.

Let me give you my opinion who just started learning 3 weeks ago. And I needed around 1 week until I could write a simple parser by hand and around 2 more until I am now being quite productive in this new language.

It is actually very simple go get into, depending on your experience. What do mean by 'threshold for beginners', what are beginners for you. Do you mean someone learning their first programming language or someone already proficient with one or multiple trying to learn Rust now? Because a lot of the features of Rust are present in other language and your mental models will be similar enough for them in Rust accelerating your learning curve. Of course someone new to programming will have problems as the features that are unique to rust (borrow, move) in addition to everything else leads to a lot of overhead sure.

My experience is with Java, Python and Javascript/Typescript as I know them sufficiently well to create programs and I also dabbled with many other languages just trying out how they feel (Scheme, Racket, Nim, Go, Haskell, C, C++, Ruby, Smalltalk). When starting with Rust it took very little time to get comfortable. The language feels very well designed and Option, Result type are very logical to use for return types. Many languages have these so depending with what you are familiar with this might be new to you but it is surely not more complicated than explaining try/catch blocks. Of course dealing with Option/Result is a bit uncomfortable in the beginning but If you just browse a bit through the standard library and learn about the `?` operator you see many patterns how to deal with them in an efficient manner (map, map_or_else, or, or_else, flatten, unwrap, unwrap_or,...). Rust has closures but now you find them in almost any language so it is not really surprising (maybe only nuances that come up with the borrow checker). Most of the standard traits and enums are quite straightforward such as `Default`, `From`, `FromStr`, `Iterator`, `Clone`, `PartialEq`, all the operator traits.

Also the borrow checker and some lifetime compiler errors are for sure a thing that can lead to thresholds and for my time spent with the language I would say there are many things I will need to learn to fully be able to grok the language. However, most of the time you can circumvent these problems by just not using lifetimes in your own code and by cloning variables. So for people that want to write really efficient code you can do this without cloning variables if you have a better understanding of them but as a beginner you have a simple escape hatch most of the time and allows you to gradually gain a better model of lifetimes/borrowing rules.

I think the only really threshold is the trait object problem. If you want to build any kind of nested structure or if you want some kind of "polymorphism" which is a natural reaction depending which programming language you have used. You will need to deal with understanding them and this might take some time.