Learning Rust, assisted by ChatGPT
I finally got around to learn Rust. Well, starting to.
It’s amazing.
I’m comparing this to learning Basic, C, C++, Erlang, Fortran, Go, Javascript, Pascal, Perl, PHP, Prolog and Python. I wouldn’t say I know all these languages well, but I do know C, C++, Go, and Python pretty well.
I can critique all these languages, but I’ve not found anything frustrating or stupid in Rust yet.
Rust is like taking C++, but all the tricky parts are now the default, and checked at compile time.
Copies
With C++11 we got move semantics, so we have to carefully disable the (usually) default-created copy constructor and copy assignments, or if you have to allow copies then every single use of the type has to be very careful to not trigger copies unless absolutely necessary.
And you have to consider exactly when RVO kicks in. And even
the best of us will sometimes get it wrong, especially with
refactors. E.g. who would have thought that adding this optimization
would suddenly trigger a copy of the potentially very heavy Obj
object, a copy that was not there before?
--- before.cc 2022-12-28 10:32:50.969273274 +0000
+++ after.cc 2022-12-28 10:32:50.969273274 +0000
@@ -1,5 +1,8 @@
Obj make_string(int i)
{
+ if (!i) [[unlikely]] {
+ return "zero as input to function";
+ }
Obj ret = foo();
return ret;
}
But it does:
// Before
make_string(int):
pushq %rbx
movq %rdi, %rbx
call foo()
movq %rbx, %rax
popq %rbx
ret
// After
.LC0:
.string "zero as input to function"
make_string(int):
pushq %rbx
movq %rdi, %rbx
subq $16, %rsp
testl %esi, %esi
je .L8
leaq 15(%rsp), %rdi
call foo()
leaq 15(%rsp), %rsi
movq %rbx, %rdi
call Obj::Obj(Obj const&) // <---- New copy in the common case.
addq $16, %rsp
movq %rbx, %rax
popq %rbx
ret
.L8:
movl $.LC0, %esi
call Obj::Obj(char const*) // <--- Expected construction for optimization.
addq $16, %rsp
movq %rbx, %rax
popq %rbx
ret
Sure, this is not as bad if Obj
is movable, but sometimes it
isn’t. Sometimes that fact is forgotten. Especially since the move may
be disabled in the future due to an added member variable to a
transitive dependency not being movable. Hell, the triggering change
may actually be:
--- before.cc 2022-12-28 11:00:02.196662142 +0000
+++ after.cc 2022-12-28 11:00:02.196662142 +0000
@@ -7,6 +7,9 @@
Obj(Obj&&) = default;
Obj& operator=(const Obj&);
Obj& operator=(Obj&&) = default;
+
+private:
+ std::array<char, 4096> arr_;
};
It would be fine if make_string()
did not optimize for 0
(because
RVO). It would be fine if Obj
were movable (because move is still
cheap, probably), but the combination could blow everything up through
a no-copy cascade.
Rust just does the right thing. A copy has to be explicitly cloned.
Sure, you could do this in C++. You could have all your types
disable copy assignment and copy constructor, and create a .clone()
member function, creating an rvalue that you then use move constructor
and move assignment from. But even then you’ll sometimes accidentally
copy due to some code you didn’t write, like std::vector
.
With Rust there’s no need to be clever with std::move
,
std::forward
, decltype(auto)
, and other fancy stuff. The only
downside is that you won’t feel as proud of yourself for getting it
right.
Aliases
Aliases can be tricky to get right. Rust enforces it at compile time. Awesome.
Const
The C++ equivalent of const
qualifier is the default, like god
intended. C++ (including my code) is full of things that should be
const
, but you just can’t bother. Like value function args. You know
you should do int foo(const int a, const std::string_view b) { … }
,
but it’s easy to skip, and hard to insist on in code reviews.
I’m pretty strict about “const
all the things”, but I’ll admit to
not always using it for value function args.
And it can’t even be used for for
loops. E.g. for (const int i = 0;
i < 10; i++) {
does not compile. In Rust i
is immutable in for i
in 1..10 {
.
Ownership
The object ownership is incredibly thought through. Rust really takes in the 50 years of C/C++ of sloppy manual ownership and… solves it.
In large code bases you have to be careful to have a good ownership
model, and maintain it. C++11 makes it easier to have a right
way. E.g. std::unique_ptr<>
means owned, raw pointer means not
owned. But those are conventions only.
GC’d languages like Go and Java just give up on thinking about ownership, and it easily becomes a big mess.
In C++ some people give up and use std::shared_ptr<>
, which makes as
much of a mess as it does in Go and Java.
Ownership anarchy in Go also makes slices… surprising. Basically Go is a bunch of puzzle pieces that don’t fit together, and ownership is just one place this bites you.
Unicode
C++ just has not solved this. At least it has the honestly to
basically call it out of scope. Python3 seems to solve it nicely with
str
. Why… just why did Go half-ass this? string
and []byte
is
kind of the same, but iterate differently, and indexing is wrong. Go
strings are like a database without a schema. It’s weak typing
pretending to be strong typing. []rune
? Really?
A thought through language
The one strength Go has is the standard library. It does most things,
in a pretty good API. But like with other aspects of Go, for every
year that passes it becomes more obvious that it has a shitty
foundation. E.g. Context
is bolted on. It’s great as a feature, but
clearly bolted on. Arch-specific code? Bolted on. Generics? Bolted on.
Learning Rust makes everything else about Go seem like a missed opportunity. This is what Go could have been. Something actually thought through, and good.
Like there’s no excuse for having null pointers in a language designed
this century. Go has two types of nil
(the second being an
interface that has a type but is a nil
pointer). And pretending to
design for parallelism, but not solving data races is a pretty big
WTF.
Go is clearly a design from someone who’s never looked at anything anybody else has been doing for the last 30 years. It doesn’t matter how smart you are, nobody is smarter than everybody.
Go feels like a language “designed” by someone who doesn’t care about making the best language for everyone, but just one that works according to the way they happen to already think.
I’ve complained about Go before, but that was 9 years ago and I now find those complaints superficial. Go’s problem are much deeper. I still stand by the complaints, though.
C++ was designed in a different time, with different requirements. C compatibility simply prevented these modern innovations. C++ is C done right. Rust is programming done right.
I didn’t mean for this to be a rant about other languages, but the way to talk about one language is to compare it to others.
So far at least Rust feels like coming home.
ChatGPT
The way I’ve been learning Rust is to read the rust programming language while writing the wp tool, and asking followup questions to ChatGPT.
ChatGPT has been a very helpful mentor. Like with everything else it’s mostly correct, but sometimes gives you something plausible looking but wrong. E.g. it tells me that I can close a child process with:
child.stdin.as_mut().unwrap().close().expect("failed to close stdin");
but you can’t (maybe I’m missing something, but ChatGPT’s example code does not compile). And it takes me a while to even figure out that it’s wrong. Google to the rescue, finding this stackoverflow question.
But it’s been very helpful for other questions. Like example syntax
for struct lifetime, or convert from Vec<u8>
to [u8]
. Or if I kind
of remember something from the book, but not exactly. ChatGPT has
usually gotten it right, and is faster than finding the right chapter.
It’s like having a private tutor, that only lies like 10% of the time. Like here:
If I were in school now I’d use ChatGPT as a tutor for any subject. It’s a force multiplier for learning things.
And yes, I’d probably use it as a co-author, too. But while it creates risk of cheating, we should not dismiss it as a learning tool. In this aspect it’s the next generation’s Wikipedia.
Back to programming
I’m a bit tired of Go not being type safe, and playing fast and loose with correctness. Again and again.
Basically Rust is what a designed modern language looks like. Go is a language haphazardly thrown together and “Yeah, I guess it kinda works”.