Reflections on Rust, and the Sand Castle Metaphor
brandur.org
brandur.org
One of the really eye-opening ones for me was building a UI library on win32 and watching it just work on Linux, OSX, Android and WASM(with some Canvas work). Obviously each platform took some work to build out their respective rendering primitives but the core engine(including embedding Lua via gcc crate) just worked.
As someone who's done x-platform stuff most of their career it was nothing short of incredible.
EDIT: Curious why people are downvoting this. Is it really so taboo to suggest that Go could be nearly as pleasant as Rust at something? Do I really need to roll out my Rust fanboy creds every time I participate in a thread on the language?
For Android all I had to do was point it to the NDK(for both Rust and the crate) and I got all my C dependencies for basically free. Anyone who's had to work with the NDK knows how nice that is. That's also with zero changes to my build script/code/ifdefs/whatever.
That's really cool indeed! Rust's build tooling is truly world class.
This article is about comparing languages more or less, though.
And of course Smalltalk images can be saved on one platform and restarted on another, but that's with a VM.
Heck, C doesn't even guarantee that a byte is exactly 8 bits.
The point I was making is that I was able to do this without a single ifdef/platform specific code without even planning for it(since it was the first time I'd cross-compiled for more than one other platform). While that's common in the VM language space seeing that in a compiled language was refreshing.
It not my fault not everyone bothers to learn them.
Why not celebrate that these concepts are making it into more and more languages? Languages that have rich, vibrant communities and ecosystems.
Seems a lot more productive to me then yelling at the kids to get off your lawn.
Those misconceptions eventually become urban myths and lead to situations like many young devs nowadays thinking that C was the very first systems language, while we old timers know for fact we had plenty of other choices.
Rust is a great language, just lets not pretend it is the first in many of its safety or portability features.
I was a big fan of Rust at first, but I bailed out a while back.
> This is especially true when it comes to the more complicated ones like moves and futures, but also true for simpler ones like borrows. What I didn’t know when I wrote about it in frustration a month ago is that it doesn’t take a little longer longer to be effective in Rust compared to other languages, it takes 10 to 20 times longer.
I think futures might be exacerbating the authors problems. There was a recent post [0] that does a good job explaining why futures are so hard and how the in-progress async/await features fix it.
Rust started out with roughly the complexity level of C++, and it's become more complex from there.
Soon people will use different dialects of the language like in C++ and like in C++ only few will be able to say that they really know the language.
How will managing huge enterprise projects will look like in future? With average developers maintaining it and not language experts like in case of Mozilla Servo?
Time will tell I suppose. I am little bit pessimistic but I hope to be proven wrong.
Rust has nowhere near the incidental complexity of C++: there's so many features/syntaxes in C++ that have surprising interactions. For various reasons (such as not aiming for direct C compatibility), Rust has been able to avoid those for the most part.
Even just initialization syntax in C++ is complicated. For instance, what does this print?
#include <iostream>
#include <string>
#include <vector>
class Foo {
public:
Foo(int a) {
std::cout << "Foo " << a << std::endl;
}
};
int main()
{
Foo paren(0);
Foo brace{0};
std::vector<int> int_paren_0(0);
std::vector<int> int_brace_0{0};
std::vector<int> int_paren_2(2);
std::vector<int> int_brace_2{2};
std::cout << "int_paren_0 " << int_paren_0.size() << std::endl;
std::cout << "int_brace_0 " << int_brace_0.size() << std::endl;
std::cout << "int_paren_2 " << int_paren_2.size() << std::endl;
std::cout << "int_brace_2 " << int_brace_2.size() << std::endl;
std::vector<std::string> string_paren_0(0);
std::vector<std::string> string_brace_0{0};
std::vector<std::string> string_paren_2(2);
std::vector<std::string> string_brace_2{2};
std::cout << "string_paren_0 " << string_paren_0.size() << std::endl;
std::cout << "string_brace_0 " << string_brace_0.size() << std::endl;
std::cout << "string_paren_2 " << string_paren_2.size() << std::endl;
std::cout << "string_brace_2 " << string_brace_2.size() << std::endl;
}
Trick question! It segfaults, because string_brace_0 (and only that one, not even string_brace_2) is trying to be a vector of size 1 with an element constructed using the std::string(const char *) constructor (i.e. the 0 literal is becoming a null pointer to pass to that constructor), and this calls strlen on a null pointer. If the string_brace_0 stuff is removed, it prints Foo 0
Foo 0
int_paren_0 0
int_brace_0 1
int_paren_2 2
int_brace_2 1
string_paren_0 0
string_paren_2 2
string_brace_2 2
That is, constructing Foo is the same with () or {}, constructing vector<int> with () is different to {} (the latter uses the explicit initializer_list constructor), and constructing a vector<string> with () is different to {} sometimes, and the same other times!Unfortunately, random interactions like this aren't an isolated occurrence in C++ (especially due to C compatibility stuff, but it can't be avoided even using only "modern C++" constructs, as above): I suspect if you name any feature of Rust, someone will be able to demonstrate that the equivalent part of C++ is significantly more complicated to use[1]. That is, C++ is probably more complex than Rust on a per-feature level, not just some nebulous whole-language complexity.
[1]: The borrow checker is possibly the only thing that this doesn't work for, but I think there's a fair argument that it is actually simpler in Rust: a C++ programmer ends up having to do something similar, in their head, with no computer assistance.
It is relatively easy to create a situation where you overflow your program. See here: https://github.com/rust-lang/rust/issues/50049
Googling around, there are many situations where bad code can lead to an overflow and your code breaking.
let buf = [0u8; 810241024];
> Rust is another step into the beyond. When finishing a feature and its test suite I’ll run my program to see it in action, but just as a formality – I already know it works. I also know that it’s going to keep working because meticulousness of the compiler is so good at catching regressions.
> I already know it works
This is the part I don't agree all of the time, but depending on the scope and size of the project, I've seeing this be true. And the compiler is really good at catching regressions, submitting PRs for Rust projects is not easy in the beginning, but it is very hard to insert a regression.
Trying to explain otherwise to them is an exercise in futility. Because ownership/borrowing.
So serious Rust project members may never claim this, but there is a large less widely experienced Rust community who have turned the claims Rust does make into something much stronger and more resistant to logic.
fn main() {
let a = 3;
assert(a == 2, "a should definitely be 2");
}
Rust doesn't mean no bugs ever, and it doesn't mean no crashes. It protects you from a very specific class of severe bugs, and it has a pretty good set of built-in primitives to help you write bug-free code in the other classes. No more, no less.For instance C's equivalent of Rust's enum would be tagged unions, however the compiler cannot enforce the semantics of tagged unions and will gladly let you access random members regardless of the tag.
Same story with the borrow checker compared to C's "whatever you say chief" approach to resource management.
Rust is definitely not the only one to offer these guarantees and some languages even go further (such as Idris for instance). The reason Rust stands out is that it can be used basically anywhere you'd use C or C++, something few other languages can claim.
The borrow checker is the primary reason I feel this way. A smaller aspect is how easy it is to write white box unit tests. I can have an equivalent of a C++ static function in Rust and run unit tests on it. For C++, I'd either have to hack up the source files for when building my unit tests or make turn the function from static to being exposed in a header so my tests can access it.
Having a GC doesn't preclude you from leaking resources(file handles, textures, etc) and you're at the mercy of whenever the finalizer decides to run once the GC has determined it's time to free an object. Usually this manifests as your Java application humming along until you hit some GC threshold cliff and then perf/memory plummets.
I also can't enforce ownership in Java which drives me up the wall. Once you hand out a ref anything is fair game and calling code is free to add that ref to the root GC set so that it never gets freed.
The comparison here was to "easier statically typed languages" (likely with an implicit "that one is likely to consider for a project these days"), with Go and Java explicitly raised for comparison. It's perfectly reasonable to respond to that with "There's no null", without explicitly acknowledging that there's also other languages (even ones before Rust) that don't have null.
People keep repeating that the popular language/framework/library of the year has $feature without acknowledging that it's not a new idea. This creates hype.
In addition the explicit error handling (together with enum error types) allow the compiler to check that you have handled every error case.
Rust has enabled me to write programs that just don't contain bugs (after an initial testing period). This is certainly acheivable with other languages, but it involves a lot of discipline, writing tests, etc. Rust makes it easy.
I'd estimate it took me about 10 times longer to implement in Rust than Python. And I don't exactly know Python well - it wasn't just extra time Googling how to do things.
A lot of it is just how restrictive the borrow checker is. Often you have to structure your code in a really weird way to satisfy it.
Dealing with strings is another pain point. I get why there is `&str` and `String`. But that doesn't explain why I can't add two `String`s together using +.
Another thing I've found is that because of the type inference it often gets really confusing whether a variable is a reference or not. Doesn't help that code completion basically doesn't work at all at the moment.
I wish there were a simpler language, like Rust (no garbage collector, no runtime), but that was a little more helpful and willing to do implicit things even if they are slightly slower than the most optimised code possible.
That doesn't surprise me at all. Writing it in C++ would probably take me a lot longer than writing it in Python too.
To address the heart of what you're getting at: I don't buy the fact that it took "10x" longer to read an XML file. I'll give you some amount of "longer" just by virtue of Rust's general strictness and lengthy semantics (like any systems language), but you can't just squash together your unfamiliarity with the language or its libraries and use that to determine the base line difficulty of implementation. To highlight this, consider a seasoned C# developer trying to read an XML file in python for the first time. It would probably take them "10x" longer in python too, and it won't be because the languages have different purposes/audiences or the languages' rules and conventions.
I'd take the view that "a little script to read an XML file, get a path from it, read a file at that location, do some regexes on it, and then update the XML and write it back to disk" doesn't need any more speed than Python, but can benefit from more correctness. I'm willing to spend a bit more implementation time than Python to get more safety than Python, but I'm not willing to spend extra time just to make it faster (and frankly I'd question the priorities of someone who would). If implementing in Rust is slower than a more error-prone language, that's not necessarily a mark against it - but if I can implement faster than Rust in a language that would also have similar safety properties then I'd rather do that, even if it meant somewhat slower runtime performance.
Why? Does this really make sense? You write it once, it runs a multitude of times. Why is the extra 30 minutes (maybe) to implement in Rust worth more than the extra CPU cycles over countless runs? It's going to be less error prone and faster.
CPU cycles are cheap - my CPU is idle most of the time - and my time is expensive. Given that the script in question is fetching a file over FTP, the time taken for that is going to swamp any compute time (even in the Python implementation). Even if you could make a version that ran instantly, how many times would I have to run the script to add up to saving 30 minutes of my time? It doesn't sound like the kind of script that's going to be in use for decades.
> It's going to be less error prone and faster.
Sure, but the overwhelming majority of the time "faster" doesn't matter. So I'm much more interested in comparing languages that have the same safety properties, and whether it's possible to get the same "less error prone" effect from a language that's easier to work in than Rust.
Personally, I don't think this is even an interesting metric. These are tools for experts, understanding expert experiences is more interesting.
[1] http://benchmarksgame.alioth.debian.org/u64q/compare.php?lan... [2] https://benchmarksgame.alioth.debian.org/u64q/compare.php?la...
stirner seems to mean something different by "competitive".
> System programming requires a great degree of hardware awareness. Its goal is to achieve efficient use of available resources, either because the software itself is performance critical (AAA video games) or because even small efficiency improvements directly transform into significant monetary savings for the service provider (cloud based word processors).
So the question is, having been able to achieve half of the allowed budget, why pay for more development effort?
Of course, if the bugdet is only achievable with a lower level language, there is no way around using it.
Out of interest what setup are you using? My editor is telling me whether a variable is a reference or not so maybe this is just a plugin you don’t have? I’m currently using Rust Enhanced (with RLS), I believe VSCode shows you this information too.
Which kind of makes me wonder if a language like the one you wished for existed would it not be better to pick one of the existing languages instead and maybe use type annotations to help in Node or Python?
I think a lot of proponents might argue that Python is better than Rust for very, very small scripts like this, but as your program grows, Rust pays its dividends quickly. This hasn't been my experience with Rust in my 4 years with it; it makes certain classes of errors harder, but I've not yet gotten to the point where the compiler is actually saving me time--the time I spend pacifying rustc is more than enough to write it in Go (or even Python) and get my extra confidence from tests. I like the idea of Rust, but it's hard for me to justify it for the applications I tend to write.
Then how do you explain the constant push for "more ergonomics"?
Which has not been my experience.
1. It really helped me to stop trying to force my code to be as pretty as possible the first time around.
2. If you are getting errors about a lifetime not living long enough in a complex expression, it can help to pull some part of it out into a helper let binding.
3. Adding an artificial scope (a curly block in the middle of another block) can be a great way to control the lifetime of a borrow.
4. clone() all the things! This is a major advantage that Rust has over other ball-and-chain languages like Haskell. When you get into a type-tetris situation in Haskell, you just have to find a way out. In Rust you can often just clone() something that you would rather not have to clone() and move on. I love being able to tell the borrow checker to go away while I concentrate on my program logic. Later I can always go back and fix it.
The fiddling with borrows isn't about being as optimised as possible, it's about being correct in the absence of garbage collection. Are you sure you need no GC and no runtime? Tasks like the script you describe sound like a perfect fit for something like OCaml.
I've rarely found this the case for me but I came from C++ and am generally used to thinking in terms of lifetimes. Could you elaborate on how you ran into problems with borrows messing up code structure?
> Dealing with strings is another pain point. I get why there is `&str` and `String`. But that doesn't explain why I can't add two `String`s together using +.
I feel like the Rust stdlib takes after C++ in making costs obvious to people and not providing shortcuts for slow operations, causing the ergonomics to scale with performance.
Something that some of us in the CLI-WG have been talking about is creating a library that interops with the standard library but instead focuses on python-like semantics at the cost of performance. This will be a big benefit for "quick scripts" and people learning the language while allowing people to still fall back to high performance idioms as determined by a profiler.
Unfortunately, this is a little lower on our priority list for now.
Because that would consume both Strings, which doesn't make sense. The impl that exists only consumes the left string, reusing its buffer.
This seems like an odd thing to complain about since if you try it the compiler will tell you exactly what you should do instead.
extern crate xpath_reader;
extern crate regex;
use std::io::prelude::*;
use std::fs::File;
use xpath_reader::Reader;
use regex::Regex;
fn main() {
let mut contents = String::new();
File::open("test.xml").expect("Unable to open the file").read_to_string(&mut contents).expect("Unable to read the file");
//println!("File contents: {}", contents);
let reader = Reader::from_str(&contents, None).expect("Cannot parse XML file");
let files: Vec<String> = reader.read("//file").expect("Cannot find file entries in XML file");
//println!("Files: {:?}", files);
for file in files.iter() {
println!("File: {}", file);
let mut contents = String::new();
File::open(file).expect("Unable to open the file").read_to_string(&mut contents).expect("Unable to read the file");
//println!("File contents: {}", contents);
let re = Regex::new(r"foo bar").expect("Cannot compile regex");
let replaced = re.replace_all(&contents, "baz").to_string();
//println!("File contents: {}", replaced);
File::create(file).expect("Unable to overwrite the file").write(replaced.as_bytes()).expect("Unable to write to file");
}
}It's not the typing that is slow - it's the constant "why can't I do it like this?" stackoverflow trips. For example - how do you open a file in read-write mode? Using File::open()? Nope!
Ok that one is just poor API design - most of the time was really in restructuring to make the borrow checker happy, and fiddling with adding and removing '&' and 'ref' to get the right types.
Maybe I misunderstand