|
|
|
|
|
by ithrow
1611 days ago
|
|
So, I don't agree with your assessment at all. Writing a large enterprisey business app in Rust will likely run faster, have fewer bugs, use less memory, and even scale out better. That's true but which is more flexible for the "ever changing living business app domain" the GP is alluding to? You seem to keep ignoring this part, flexibility matters. In rust is easy to code yourself into a corner and spent lots of time rewriting stuff over and over. |
|
I guess the problem is that I don't know what we mean by "flexible". The GP did mention lifetimes around the same part of their comment, so I assume that there's some concern about business requirements changing in some way, and that Rust's lifetimes would get in the way of adapting to code to meet the new requirement.
Is this also what you mean by "code yourself into a corner"? Or are you thinking of a superset of that?
When we say "flexible" are we talking about the language being opinionated about the style of code we write or are we talking about the language making it harder to be agile in the face of requirement changes? It sounds like we're talking about the latter.
First, let me repeat myself that I don't believe Rust is the ideal enterprisey web app language. There's almost no reason that a web-app benefits from a language not having garbage collection or automatic ref-counting (like Swift).
But, I'm not backing down from my assertion that Rust is still probably a better web app language than Java, if we're willing to ignore Java's ecosystem's 30 year and billions of dollar head-start for niche, vendor-specific, libraries. Or, phrased another way, just because FooCorp gave you a jar file to connect to their smart sex swing, that doesn't make Java a better language in a fundamental sense, even if it does force your hand from a business and engineering perspective.
So, since I don't actually know what we're talking about with "flexibility", I'll just ramble about a few things.
First, lifetimes. Lifetimes are scary. But, I honestly don't see how or why lifetimes should be an issue in a high-level application, like a web app. If you have any specific scenarios, examples, or lived experiences, please share. Let me explain some of my experience with writing a couple of web services in Rust, with respect to lifetimes.
I've been using an http server called Actix-Web when I do Rust web stuff. It uses the same architecture style as Vert.x: it spins up N reactors (where N = number of CPUs by default) and each reactor is single-threaded and concurrent, which means that once a Request is routed to an available reactor, it never leaves that thread. This means that there are no complex lifetime issues with handling a Request- all of your logic can be single-threaded and treats the Request as having "static" lifetime (the Request outlives your handler function, so as far as your function knows, the Request lives forever. The caveat is that you borrow its content such as headers, uri, etc and would need to take copies if you wanted to send them elsewhere). I've never had non-trivial issues with lifetimes when it comes to the basics of request processing.
The SQL query builder I referred to in a previous comment gives us a transaction object with a legit lifetime, because it uses RAII to close the transactions and to return connections to the connection pool. The only time this has caused me grief was when I was trying to be clever by implementing a type class around transactions for some reason that I don't even remember now. I don't see how or why a changed business requirement would require us to extend a transaction's lifetime explicitly.
A further point: it's fairly easy to leak resources in Java because you can't do RAII except with the try-with-resources stuff. But, you can easily forget to try-with-resources and leak. Or, on the opposite side, since an object can still be referenced after you close it, you could pass an already-closed connection around and cause an error far from where the connection was first obtained and/or closed. In Rust, such mistakes would never compile.
Really, in an idiomatic Rust app, I would expect that the only place where you'd see explicit lifetimes is from RAII. Everything else is either going to be plain-old-data or some kind of ever-living actor/service. I'd be surprised to know that a high-level app is actively managing lifetimes of pretty much anything.
I'm not saying that it's impossible to end up with some ugly function signatures because of lifetimes. I can imagine writing a function that takes two parameters with independent lifetimes. But, I don't know why it would limit your agility.
Moving away from lifetimes.
Rust does preclude certain designs and architectures. You can't really do self-referencing structs (easily/simply/whatever), so you're not going to see a complex web of sibling objects referencing grand-parent objects, referencing the town they live in, referencing the grand-child objects. In this sense, yes, Rust is less flexible, and if you try to write Java style code in Rust, it's going to be painful. But does this make a Rust app less agile? In my opinion, no. Sure, you need to write your code in a different style than you would with a different language. And, sure, there's a learning curve to writing "good" Rust code, but are you willing to tell me that there isn't a learning curve to writing enterprise Java app style? The millions of pages printed and watts burned by people teaching and learning Gang Of Four design patterns, and Domain Driven Design, and Clean Architecture would suggest otherwise. Then the millions of watts burned on StackOverflow posts about == vs .equals(), and how static methods work with inheritance, and how to implement a generic interface for multiple types (you don't), and what the difference is between DAOs and Repositories and Services, etc, would also suggest otherwise.
In fact, here are some things that have made my Rust code MORE agile:
* You know how people praise static typing as allowing more confidence in refactoring? The idea of doing a big refactor of a Python or JavaScript code base makes me break into a cold sweat. Rust's type system is way stricter than Java's and I'm much more confident that when I refactor Rust code, I won't accidentally introduce a race condition or resource leak.
* If I want to extend a type with new functionality, I don't even have to own that type. Or, if I do, I don't even have to change the original file. I can define a new trait *and* write the implementation of that trait near the code that uses it. How do you do it in Java? You write your new interface and then write a wrapper class that delegates to the original class. Except now, you can't use that wrapper class in place of the original- you have to convert back and forth. Not so in Rust. Much more "flexible", IMO.
* modules > packages for namespacing and visibility.
* traits allow me to define/require "static" methods on implementing types.
* If you have two interfaces, Foo and Bar, in Java, and you want to write some code that does something special for a type that is both Foo and Bar, what do you do? It's been a while, but if I remember correctly, you have to define a new interface called FooBar that extends Foo and Bar and you have to go find every class that implements both Foo and Bar, and change them to implement FooBar, instead. In Rust, I can just write a function: `fn <T: Foo + Bar> do_stuff(o: T)`. Done. Didn't have to define a new type, didn't have to touch old stable code, etc.
* I can implement a generic trait for multiple type parameters (eat that, Comparable<T>!).
All of the above have allowed me to add or change functionality with minimal added code and minimal regressions.
Java being flexible is a truism, IMO. It's not flexible. We've just mastered it to the point that we don't even try to do things that we know are impossible, but are totally reasonable to want to do. We've gotten so used to its restrictions and limitations that we don't even see them anymore, or we just pretend like it's actually better this way.