But when I do work Go excels in (web architecture, especially), I'm not particularly keen on using Rust or Zig in it's place. While it's perfectly possible to build a Postgres-fronting, redis-cached microservice to manage users, auth, etc in Rust, it's going to be an exercise in frustration compared to something more streamlined for that task.
It's fine to explore with- and write proofs of concepts in- Rust.
In many ways, it's better suited than Python, Ruby, JS or PHP for this, for several reasons. I have too little production experience with go to know how it does here.
The Rust type checker, and easy, built-in test framework, make refactoring easy and certain. Much more than the common "fingers crossed let's deploy" refactoring I've experienced with Ruby and Python. And way more than the Yolo methodology commonly employed in JS and PHP. And fearless refactoring is a requirement for me to experiment and explore. Because once I know what and how I want it, refactoring is needed.
A good, fast testing framework helps me explore. Efficient and iterative. Rust (and Go) have much to offer here, whereas in Python, Ruby and even more so in PHP or JS, testing is an afterthought, third party, bring your own setup. In rust I can start writing a test immediately after 'cargo init experiment'. But I understand not for everyone exploration==tests.
The rediculous ease of deployment of both Rust and Go compared to Python, JS, PHP and worst, Ruby, is crucial when I'm figuring out if people need what I'm building. 'cargo build --release && scp target/build/foo example.com:/use/bin' and I'm hosted and deployed. It's less work than getting a Heroku connected to GitHub. For me, releasing fast, early and often is a crucial feature when exploring. Rust (and Go) have solved this. In fact, this is my main reason to build in Rust: I'm just fed up managing complex and ever breaking runtimes, (pip-, rb-, nv) envs, complex and ever breaking CI.
With rust, my initial versions just have ARCs, Box-es, unwrap()s and clone()s all over the place. It's how I circumvent the frustration, yet know the app is 'correct'. All those clones, unwraps, boxes, Arcs etc act as TODO's, bit without the "this will probably cause corrupted data, fix me!", that my TODOs in Ruby or Python came with when exploring.
IMO those who say Rust isn't suitable for hacking seem to try to conform to best practices right away when really, dealing with lifetimes and excessive clones should be part of the optimization step, not the ideation and experimentation steps. Rust is as high level as you want it to be. Hell, I write web backends and sometimes even frontends in just Rust and I never worry about memory until I want to optimize, if I even want to optimize.
This part of the comment deserves to be highlighted and mentioned more. I think this exactly the way to "hack something together quickly" in rust.
After that, incorporating correctness seems to be as simple as grepping with each unwrap and "dealing with it".
With the above approach, Rust is a great prototyping language as well.
With unwrap, replacing the unwrap with an expect signals you've accepted that an unwrap is ok in that piece of code. I'd like a similar convention for clone.
Perhaps it's more a question of background? Rust contains many features I already know from the ML family + it formalizes the way I've already been thinking about memory since the early 90's.
If you ever have to change lifetimes of existing functions/structs on a non-trivial Rust code base, you're screwed.
I forgot that I had also written a small cycle-by-cycle emulator of a simple CPU (8-bit data bus, 16-bit addr bus, 8 16-bit GPRs + a 16-bit PC + a single condition flag + an interrupt flag, timer interrupts, conditional jumps, call, ret, iret, mov, add, terminal I/O). That was also surprisingly easy.
I would like to use generators or something similar for a more realistic cycle-by-cycle emulator but I haven't gotten around to playing with that yet.
Generators and various kinds of coroutines are, yet again, just formalizations of the way I have been thinking for years.
Maybe other domains would be more challenging but I seriously doubt it. I do know that it will take me a couple more months before I'm really proficient in Rust -- including multi-threading -- and no longer fear macros.
If I’m doing something for fun I always go to Python though. Just makes everything seem incredibly natural, and given all the fancy interpreters it can be pretty fast.
You just described my reasons for switching from Rust to Go better than I ever have.
But having made a microservice in rust it is the most stable software I ever written, you are basically paying upfront any null reference and thread safety issue I would have needed to debug. It probably did not save me time, but it definitely was a better experience to write
In real world use ridiculously so.
The lack of exceptions, Option type or ability to chain or manipulate errors means every line ends up wrapped in a 90s style "if error" check. Which ends up making code less robust than languages with more modern approaches like Rust, Scala, Swift etc.
The method type signatures clearly specify what exceptions will be thrown.
And the compiler requires you to deliberately handle those exceptions.
But I wouldn't consider Java to be the best approach for error handling anyway.
What makes it '90s style' other than you don't like it?
Calling something "90s style" and its alternative "modern" is a thought-terminating cliche.
- it returns both a return value and the error value, not xor. The convention is to use the error value only if returned, but sometimes the other “slot” is also used. If you squint a bit, this is exactly what errno does in C. The better, ML-y or exception-y way gives you a sum type instead, which can’t be mishandled.
- you have to handle it in-place with no convenient shorthand allowing for handling it at another layer (and usually that’s the only meaningful way forward). Also, error handling is a cross-cutting concern, squeezing it inside your business logic is (in my opinion) a bad thing.
Whether or not it's better to have a sum type (IMO it isn't), what Go has isn't anything like errno. errno is an ugly wart on the POSIX interface, which is otherwise quite nice. For example, Linux system calls directly return negative error codes, which is a much better way of handling errors, and then the libc has to set errno and return -1. Awful.
>- you have to handle it in-place with no convenient shorthand allowing for handling it at another layer (and usually that’s the only meaningful way forward). Also, error handling is a cross-cutting concern, squeezing it inside your business logic is (in my opinion) a bad thing.
That you say this and then advocate for optional<T>/result<t,e> is insane. That's exactly what a sum type does: it squeezes error handling into your business logic. And no, papering over that with ugly ? operators everywhere is not a solution, it is syntactic sugar. Error handling is part of the logic of your program. It is not just some 'ah we'll return it' thing. Many operations can fail and many errors are recoverable.
Which is an implementation detail, has nothing to with its semantics what we discuss here. Certain C compilers do wild things with errno as well.
I’m not advocating for optional/result types, I personally believe that checked exceptions are the ideal solution, but it is a thoroughly underutilized tool in PL designs (Java has it, though it has many warts, but that is not inherent to the concept). Hopefully with the wake of effect-typed languages it could change.
With that said, in Rust’s low-level case it makes sense to avoid exceptions, and sum types are an okay solution, especially if it has a handful macro for bubbling up. What go has is just straight up wrong.
Monadic error handling (Rust's Option<T>/Result<T, E>) avoids that issue by putting the error inside the return type as a normal value.
(Which is why, on multiple occasions, I've seen people call Result<T, E> "checked exceptions done right/properly".)
But they fail at including a stacktrace, autobubbling up, auto-unwrap and customizable "hit radius" with a try-catch block (okay, I'm sure some Monadic construct allows for that, but e.g. Rust's version does not).
(Plus, true exception handling is messy for a language that cares so much about FFI. Look at the discussions surrounding how panic! unwinding should behave at FFI boundaries.)
2. I think completely automatic bubbling would be a misfeature, and the `?` operator is a good balance between concise ease-of-use and the Rust focus on "code is read far more often than it's written".
3. Again, the `?` operator. I think completely automatic unwrap would also be a massive footgun and comprehension hazard.
4. As mentioned in the other reply, try blocks are in development. (See my previous comment about Rust adopting a Minimum Viable Product approach to v1.0)
https://caniuse.rs/ is a great illustration of how far we've already come and how minimal Rust 1.0 was.
Some examples (not an exhaustive list):
1. Fibers/stackful coroutines (See "Fibers under the magnifying glass" by Gor Nishanov for why they were hugely popular in the 90s and then got abandoned by all the major OSes and programming languages → http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p136...)
2. Garbage collection (The 90s were the height of GC hype, to the point that the JVM still struggles with "Don't worry. GC research will make it fast." design decisions.)
3. Error handling that feels like "Well, we know that exceptions were a mistake, so the only other option is C-style return values... but, hey, we can at least follow 90s Python's lead in supporting returning tuples." (Granted, exceptions DO bake in a fundamental assumption that all the world can be treated as single-threaded execution amenable to transactional rollback. → https://www.lighterra.com/papers/exceptionsharmful/)
4. No generics until they were pushed into it by things like people writing preprocessors that use the Canadian Aboriginal Syllabics characters that look like angle brackets. (https://www.reddit.com/r/rust/comments/5penft/parallelizing_...)
5. No sum types (Bear in mind that sum types have been around since ALGOL 68 and, on many fronts, C was a step backward that the entire industry copied)
6. Dependency management that was only a hair above "just `git clone` it into your project" for most of Go's lifetime
An example of such a reference would be this quote by rfiat at https://news.ycombinator.com/item?id=31603329
> It's interesting that you find Go to be Rust-adjacent. Setting aside the USPs of each language (goroutines and borrow checker respectively) and just speaking about the general experience of writing code, I find Go painful for all the reasons that I find Rust pleasant.
> The best summary I can give of Rust is that it was designed by people who learned from the mistakes of the programming languages that came before it. The best summary I can give of Go is to quote Rob Pike's response [0] to a request for syntax highlighting in the Go playground:
>> Syntax highlighting is juvenile. When I was a child, I was taught arithmetic using colored rods (http://en.wikipedia.org/wiki/Cuisenaire_rods). I grew up and today I use monochromatic numerals.
> [0] https://groups.google.com/g/golang-nuts/c/hJHCAaiL0so/m/kG3B...
dgunay's reply in that same thread ended on the summarization "I like Go and I feel very productive in it, but its commitment to simplicity is dogmatic in many ways and it very much is missing milestone advancements in PLs from the past several decades. It could easily have been made in the 90s."
This. It's Rust, not Go, that has replaced Python for me in any use-case more than a few lines long where the ecosystem is ready (i.e. when I don't need a GUI (PyQt/PySide), SQL (SQLAlchemy+Alembic and Django ORM autogenerate draft migrations by diffing the schema against the DB), or complex web stuff (Django's giant ecosystem of reusable components)), because...
1. Boilerplate like `if err == nil` is demotivating
2. "If it compiles, it works" is very motivating and, aside from having `#[test]`, a type system constraint is worth a thousand unit tests.
Heck, as efforts continue toward things like making LLD the default linker (40-50% reduction in link times) and making an official/stock interpretation of the rust-script utility (https://rust-script.org/), and as I work toward replacing my dual-core AMD CPU from 2011, Rust is going to push even further into being what I use to write "shell scripts" because "It works. Don't ** with it" is the number one psychological effect I have to push back against in other languages.
Rust code lets be confident that when I make changes as I explore the problem/solution space that my program will actually work.
I write Rust professionally and what you hate about Rust is opposite to why I love it so much. Especially in larger projects, I have never in all my career had an easier time hacking and iterating. I'm infinitely more confident in writing a ton of lines that will work once I compile. In Python and Go, I could end up chasing silly bugs and waste time debugging and tracing to find that I made a typo or ran into a language quirk that gave me an unexpected nil pointer. That situation is almost non-existent in Rust, it's just me and the problem. Rust is honest and upfront about its quirks and will yell at you about it before you have a hard to find bug in production.
Those last two sentences aren't 100% true. Advanced Rust can be tough and you absolutely will fight with it. But to be perfectly honest, this gets rare over time and usually it comes from a lack of understanding. When you finally do understand, it's another tool to add to your belt that makes you a better programmer.
Only thing I'll add is. 8 times out of 10 if you are reaching for advanced rust, you are over engineering. Most software projects don't require it and when they do, you should always question whether there's a simpler more maintainable way or make a other design decision. There is no award for least lines of code, or most utilized compiler. Software is social.
That's Go's biggest strength over Rust and Scala. Good luck getting job security in that language. It takes a few years to really know what I'm saying but in my opinion it's the difference between a senior engineer and a non senior.
And that's absolutely not how you do it in Rust either. Once you've internalized the Rules of the language, you can definitely grok your solution in Rust the same way you'd do in Python, the biggest difference being that Rust will help you a lot more when refactoring your code to the next iteration, so you'd be more efficient over a few days of work in Rust than in Python.
The big caveat being that you need to actually learn the language first instead of pretending you're a polyglot developer and you can write code in any language without having to spend time learning it, but maybe you'd argue that nobody does that either…
I prefer go for cloud integrations because they tend to have the best implementations and most solid APIs. That's about it though.
Python is a good bash replacement, but I don't like writing large projects in it. It's hard to maintain and the errors that come from production are never straightforward. Good for some medium scale mathy compute projects though.
C++ is where I write most math code after prototyping it elsewhere(R or Python)
Rust for me is the answer to most things except, it's ecosystem is a bit weak, and it's a bit of a clout chaser language(not as bad as others though)
I find Rust to be unintuitive, and I don't think it is a language designed to be a "problem solver.
So, what do I mean by a problem solver? If you have a problem and you write code by exploring different solutions, continuously improve, and write hacky stuff where the goal is to simply solve a problem.
Rust is not a problem solver; it is going to fight you all the way through because of its emphasis on programmatic correctness. With Rust, you have to have the solution first, consider the language-specific caveats, and write something in such a way that you don't go back and have to improve it. I suppose that is how programming is supposed to be done - in a clear and structured manner. But let's admit it, nobody does that.
On the other hand, Go is a language that sits between Python and Rust. Python is THE problem solver language, and Rust is an elegant and good etiquette language. Go just works as a hackable language with the benefits of a compiled language and type support.
My plan with Rust vs Go for the time being:
- If I need to solve a problem and can do continuous improvement on the codebase → Go - If I have the problem solved, I have the resources to do a rewrite → Rust