I've spent the last few months porting the guts of a 100 KLOC PHP command-line utility I wrote to Rust. Thanks to the wonderful Rust documentation it's been a mostly painless endeavour.
- a bunch of time thinking about the borrow checker
Quick iteration and concise readable code are much more important in web-land than in system programming land, and those are key reasons why I don't think many will be abandoning interpreted langauges for Rust.
I sometimes wonder whether people adopt Rust/Haskell/ReasonML for front-end work because they want it to feel like more of an effort than it otherwise would.
You may want to check out Crystal https://crystal-lang.org/ -- you will get all of the positives you describe, while avoiding perhaps 3 out of 4 of your listed negatives. (I think your "ease of iteration" point will still probably win for PHP vs any compiled language.)
Also, I'm very curious: how does the KLOC count compare for the PHP and Rust versions?
> I think your "ease of iteration" point will still probably win for PHP vs any compiled language.
While I don't think you can beat straight php/js/python for this, there's still a lot of room for improvement over the rust/c++ magnitude of compile times.
For example Go/OCaml/Common Lisp (at least, SBCL is a compiler, there are lisp interpreters), all have reasonably quick compile times. And the latter two have repls, which really does help improve the iteration cycle.
I haven't had to play with crystal, where does it fall on this spectrum?
> how does the KLOC count compare for the PHP and Rust versions
I don't have exact numbers since it's not a straight port — the original PHP code was written to analyse a slightly different language than the Rust version I'm building.
I'd estimate that Rust requires you to be about 10% more verbose than PHP. Main annoyances vs PHP: no default argument values, and only a single let allowed inside an if conditional.
My company has its code basically split in half between Python and Rust. Our Python code uses mypy pretty aggressively. The iteration speed difference is... drastic. Rust far outshines Python in terms of our ability to get something done, maintain it, and iterate on it over time.
Did you mean to write that python beats rust? I find it hard to believe Rust is faster to 'get something done' than Python. Happy to be proven wrong, of course.
That is something I would find hard to believe, if you mean iteration of changes. I have coordinated a few rust rewrites, and at least seeing engineers live programming, it takes far far longer to change and compile Rust code than python.
Could you talk more about your company's process or how you mean?
The time it takes to run `cargo check` is nothing compared to dealing with bugs, terrible tooling, weak errors, etc. All of those areas have been radically better with Rust.
As an example, type errors in Rust are radically better than mypy, it's no competition. In general rust's type system is just way better, we have to use Any all over the place because mypy can't even handle a Json type (no recursive types at all).
There's nothing that Python really does better in terms of iteration that I can think of. Obviously you don't have to compile code, but `cargo check` is seconds, that is not the bottleneck. We use hypothesis in Python and quickcheck in Rust, cargo is way better than pip, working with loosely typed data is about the same in Rust, maybe a bit better, and working with strictly typed data is wayyyy better.
The tooling for working with crates, having an actual lockfile, higher quality libraries, etc. It just all makes Rust more productive.
We've run into maybe one rust footgun, which is that binding to _ drops the value immediately. We've run into so many Python footguns, like, did you know that in Python if you want to do circular imports you need to put your imports at the bottom of the file? Or use absolute imports everywhere? Neither of which seem to work 100% of the time by the way, something that makes writing graph data structures quite cumbersome. The other day we ran into a stray comma turning a value into a tuple. The papercuts and footguns across the tooling and language just add up really fast and basically don't exist in Rust.
I don't know how people get stuck on the borrow checker, frankly. It's one thing if you're very new to the language, but within weeks it should be very simple to avoid those errors. I maybe hit a "fuck, what" borrow checker issue once a year, in which case I hit up the Discord, they fix it, and I file an issue with rustc so that the error message will get improved.
This is based on about 7 years of Rust and Python experience, with 6 years of professional experience in Python and 3 in Rust.
I'd like to hear more about the Rust rewrites you've coordinated. Why do you think Rust took longer to change than Python?
I've felt this a little bit, but can only theorize why; the borrow checker makes mutability into a viral leaky abstraction, such that a change over here can sometimes cause widespread refactors on other components that would be decoupled in other languages.
Jury's still out on whether that's healthy long-term for a codebase, especially once the original writers have left. Jury's also still out on whether it encourages us into better architectures to begin with. Maybe it evens out, maybe not!
> Why do you think Rust took longer to change than Python?
Potentially the issues we ran into were due to the inexperience of the team, Rust despite being ~12 years old, most of the programmers we were working with had less than 2 years of experience with it. So if you have a more experienced team, your mileage may vary.
Given that, I think it has to do with the typing system, it is a lot faster to change your structs in python and propagate the changes as needed than in Rust. Asking for another field to be displayed on a web dashboard could take ~1 hour for a team we had working in Django, and ~2 days for a team working in Rust. Of course the projects in question were wildly different and a lot of other caveats, but still in general we found that Python was just much much faster to make changes with. I was not directly involved in any of the programming, but did review the changes, so my perspective may not be the correct one.
That's great for you and your team, but looking at https://github.com/grapl-security/grapl it seems like your needs are pretty different from most web developers.
It's more a CRUD app that has many features added in the first six months, and fewer features added thereafter, and then a few months go by and a new developer takes over and has to figure out how to add yet another feature.
That model favours easy-to-google frameworks with wide ecosystems.
I imagine that especially for a command line utility you also gained an easier deployment story when using rust, given the static linking and lack of interpreter. Requiring PHP to be installed to use a tool has caused me issues, especially with some of the recent PHP 8 updates introducing breaking changes. It’s a better story for PHP on a server where it’s more centrally managed.
The deployment story of the PHP tool in question is pretty good thanks to the semi-official Composer package management system (it hasn't been officially endorsed, but everybody uses it).
People just run `composer install --dev vimeo/psalm` and it works. It can also run on any web server that runs PHP, so I have it running on psalm.dev
OfC the Rust equivalent is very easy to package, and also runs reliably in the browser via WASM (currently ~1.5MB compressed).
> I sometimes wonder whether people adopt Rust/Haskell/ReasonML for front-end work because they want it to feel like more of an effort than it otherwise would.
That's ridiculous. There's a lot of garbage to unpack in that sentence, but for example Rust's borrow checker provides valuable guarantees about your code and eliminates entire classes of bugs. It's not just wasted time. Haskell's purity and lazy semantics allows for equational reasoning and more composable abstractions. ReasonML makes it really hard to accidentally write a bug that crashes the program. Etc.
Just because you don't see why those things are valuable doesn't mean they aren't.
Rust code is significantly more readable. Use the type system to your advantage.
> - - really smart type inference
Rust has really smart type inference too and only rarely ask for an explicit type. I've found that I prefer annotating my types. It adds to documentation and readability.
> - a bunch of time thinking about the borrow checker
You can use Arc/Mutex to overcome some of these and review them in a refactoring.
Rust has smart type checking. The really smart stuff is (IMO) the beauty of the borrow checker.
In PHP, modern typecheckers can verify that the first element always exists on an array after an emptiness check:
if ($some_arr) {
echo reset($arr);
}
Whereas in Rust you have to explicitly unwrap:
if !some_vec.empty() {
print!("{}", some_vec.first().unwrap());
// .. more code
}
The Rust version requires you to use your intuition to figure out that `unwrap()` will never panic here.
In my Rust port I consistently rely on the PHP typechecker's knowledge of the equivalent code when using `unwrap()`, because it knows which array fetches are safe.
> At the cost of introducing a bug/technical debt.
At the cost of shipping a product!
-------
Edit: user ibraheemdev proposes this Rust equivalent which more closely matches the idealised pseudocode, and type-checks:
if let [first, ..] = &some_vec {
print!("{}", first);
// .. more code
}
It's preferable IMO to
if let Some(first) = some_vec.first() {
because the latter relies on a property of a non-empty vec, and checks that property, rather than checking the non-emptiness of the collection.
> The Rust version requires you to use your intuition to figure out that `unwrap()` will never panic here.
You should not use `unwrap` in a production product. It's there for prototyping and I think it's a mistake they have it (though the language already requires a good upfront time investment as it is). Use `?` and have your error propagate accordingly to the top of the chain. Have your own error types and convert from other error types.
if !some_vec.empty() {
print!("{}", some_vec.first().unwrap());
// .. more code
}
I don't know why you would check the first element of a vector. Use Rust type system to your advantage, and don't "convert" PHP code into Rust. Re-design your program. Also you don't need to check that the Vector is empty if you are using "first". "first" returns an Option with None if your vector is empty. My guess is that you have yet to exhaust the idioms Rust offers and approaching it in a PHP-like manner.
> > At the cost of introducing a bug/technical debt.
We tried this style in one of Google Earth's packages for a while (using an internal C++ class similar to Rust's Result). So many operations could theoretically fail (e.g. indexing out of bounds or an expected key not existing) that pretty much every single function in that package returned a Result. These "unexpected" errors would just fly up the stack, manually unwinding it, because nobody could really do anything with them. Once we had that, I really questioned some of our policies.
I can imagine a language where panics don't crash the entire application, but instead blast away the surrounding "region" of memory, leaving the rest of the program intact and able to keep running. Similar to catch_unwind, but if the language is aware of region boundaries, and only allows certain methods of communication between them, it would leave the rest of the memory in much more predictable possible states.
It may sound surprising, but it's actually possible with the right language constructs (Erlang folks know what I'm talking about!) and that way, we could once again have panics for unexpected errors, and Results for expected errors.
> don't "convert" PHP code into Rust. Re-design your program.
Trust me, I am!
For example, I'm rewriting all the (very-efficient) PHP regex code to a much more convoluted version because Regex::new is so costly.
Less sarcastically, I am embracing the Rust Way where necessary. My complaint is that the Rust Way sometimes forces me to depart from the idealised pseudocode version of a given algorithm much more drastically than PHP does.
> Yes, but IMO that makes the code a bit more abstract:
> You've left behind an explicit "is this collection non-empty" and you're instead relying on a property of a non-empty collection.
More abstract for who? I think virtually all Rust programmers would easily understand the `if let` snippet, virtually all PHP programmers would understand the PHP snippet, and virtually all programmers of any language would understand that a non-empty array has a first element. I'm not at all convinced that most programmers would correctly guess that a function called `reset` is used to access the first element in an array though.
A collection is non-empty if and only if it has a first element. Therefore checking for non-emptiness is the same as checking for the existence of a first element.
I think that's obviously a matter of opinion. I don't think Rust is hard to read at all, nor do I think of it as being particularly sigil heavy, but that's me. I think the syntax is probably unfamiliar for some, and familiarity is almost always what people mean when they say "readability".
Coming from c++, Rust was pretty familiar. PHP isn't that hard to read either.
Most Rust programmers just never, ever have to deal with pointers like this. And most code in the wild looks nothing like this. Yes, it's there if you need it, but in practice you will never read anything like it.
Readability is the most suspect of all the anti-Rust arguments to me, because, as others have noted, it's almost always about familiarity. The most senior parent comment is someone explaining how their 100KLOC PHP command line program(!) was more concise and readable than Rust! PHP as the exemplar of readability. Think about that.
Look -- if I really loved PHP and was forced to write Rust, I might really hate it. But I think the world of people who really love PHP is pretty small compared to the world of people who have lived with PHP because that was what was familiar. Folks let's climb off the bus to crazy town.
Depends. Rust is quite readable for a low-level language that does expose every little bit of detail of memory-management. But I do agree that we should not over-promise its readibility.
A bit of one, a bit of the other. It's fairly well-optimised. I think the big remaining optimisation will be to de-allocate memory less frequently — the program creates and destroys millions of structs representing types. 20% of the runtime is taken up iterating over and dropping collections of type-related structs.
Yes — that's after a ton of Rust-centric optimisation. About 25% of the runtime is currently consumed with deallocating memory (there are a lot of heavily-nested data structures getting cleaned up), so there's definitely some more work to be done to reduce cloning.
When a person converts their 100 KLOC PHP command line utility(!) to Rust and that's not a bigger red flag than their negatives list, it's a world gone mad.
> When a person converts their 100 KLOC PHP command line utility(!) to Rust and that's not a bigger red flag than their negatives list, it's a world gone mad.
I'm not converting, I'm just porting a good chunk of it for a slightly different purpose.
Not sure what's a red flag -- the original PHP utility has 17 million downloads and people are pretty happy with it.
It was a joke. It's great that people like your utility, and that you're trying Rust.
The source of my amusement was -- I really, really don't see the world the same way as you do.
I won't take your negatives point by point, but, for instance, familiarity and readability do really seem to be in the eye of the beholder, because, to my eyes, to call PHP concise and readable, especially as compared to Rust, is a spit take. PHP is emphatically not readable to me. Writing a 100KLOC project in PHP? Ugh. Yeah, you do you, but not this guy.
What I've gained as a result:
- execution speed (about 3x faster, single-threaded)
- better-documented data structures
Things I've lost:
- ease of iteration
- concise, readable code
- really smart type inference
- a bunch of time thinking about the borrow checker
Quick iteration and concise readable code are much more important in web-land than in system programming land, and those are key reasons why I don't think many will be abandoning interpreted langauges for Rust.
I sometimes wonder whether people adopt Rust/Haskell/ReasonML for front-end work because they want it to feel like more of an effort than it otherwise would.