Hacker News new | past | comments | ask | show | jobs | submit login
Rust Is a Scalable Language (matklad.github.io)
142 points by todsacerdoti on March 28, 2023 | hide | past | favorite | 128 comments



> And you would only need Rust — while it excels in the lowest half of the stack, it’s pretty ok everywhere else too.

A younger me, in search of a new language to learn, would have been turned off by something that is only "pretty ok"

But 20 years later, something that is "pretty ok" is exactly what I'm looking for. My experience has been that finding the perfect language (or perfect anything) is futile, and all I want is something that let's me solve my problems, don't get in my way, and don't really surprise me.

For a long while, python was my go-to "pretty ok" language, but eventually it stopped being "pretty ok" for me. Rust has now taken that spot.


It's not popular among the crowd here, but my favourite "pretty ok" language is C#. It's not really the best at anything, but it's pretty good for most use cases.

That being said, because there's no one perfect language, it's handy to have a few in your arsenal. As much as I like C#, I'd still reach for Python for any data or ML type use cases.


I do love C# as a language, and I've been wanting to reach for it more for tooling and backend things at work, now that its cross platform is good. (I've mostly used it for Windows GUI apps previously). I'm more confident that any random developer can work on a C# program than a Rust one.

But it has felt like the FOSS library ecosystem for .NET is not quite as good as it is for Rust. C# is far older than Rust, and NuGet apparently has 3.5x as many packages as crates.io has crates. Despite that, my experience has been that I'm more likely to find a good Rust library for any random thing I want to do. Rustdoc documentation isn't amazing but it's consistent, and venturing into other ecosystems usually reminds me that things can be worse.


C# is, in my opinion, a fantastic language. I haven’t used it in a long time, but the “ergonomics” were really nice.

The downside of C# was microsoft, but maybe thats changed now? I know they made a lot of effort to make the .NET framework more cross platform.


The problem is the .NET ecosystem. Most of the truly useful frameworks and libraries are either commercial or freemium, and Microsoft is happy to play along and support this cottage industry instead of releasing free in-house versions and crushing the overpriced third party ecosystem.

Another issue is that Microsoft 's first party libraries are now increasingly designed to funnel you into Azure services. The language is great, but the ecosystem is rotten.


I think this very much depends on what you're doing. If it's the typical web app, pretty much everything you'll want is open-source, and most of the time there's some first-party MS thing for what you need (auth? caching? ORM?). We use it at work and don't use any commercial libraries and don't have problems, although it's your typical microservice/distributed pattern. I'm guessing for developing desktop software there's a lot more gremlins.

It actually helps a lot that everything's so open source, because often companies will spit out a SDK for .NET to check a box and not have good docs around it because most of their customers aren't using .NET except for a few enterprise whales.

> Microsoft 's first party libraries are now increasingly designed to funnel you into Azure services.

Outside of the solution for scaling Blazor being "Azure SignalIR service" I'm having trouble with this one too. Their docs often call out Azure, but there's not a ton of bias in support (again, totally might just be that I don't get in the weeds enough).


Their official ASP.NET OAuth documentation requires you to either pay for the third party Duende IdentityServer or use Azure cloud services.

https://learn.microsoft.com/en-us/aspnet/core/security/authe...


It says you can use AAD or IdentityServer as OIDC providers, but the doc goes on to explain how to do it yourself with Entity Framework storing user data in a DB and the open source Microsoft.AspNetCore.Identity hooking it into the app.

It would be odd if they only gave like Cognito as an example (especially considering AAD is free). And I didn’t know people really used IdentityServer as much in new apps, there’s other ways to do that.


In most use cases with a separate client (e.g. in React), it's easier to use a separate OAuth server. Otherwise you will end up having to change the default cookie settings.


The downside of c# is it's easy until you run into the 80 flavors of async it has.


Async is a wart, period. It’s an ugly hack around poorly scaling threading abstractions. It basically implements light weight threading but in a way that forces the developer to constantly think about it and clutters up the language with ugly complex async crap.

Go is the only language that gets concurrency right.

Rust async is ugly but it’s ugly because async is ugly and as a systems language there are limits to how much of that ugliness it can hide.

If we could fix threads to scale better at the OS or core library level we’d save insane amounts of developer time.


> Go is the only language that gets concurrency right

Java and Elixir both have taken the non-async approach. C# is actively looking into it as well now too: https://twitter.com/davidfowl/status/1532880744732758018


> Go is the only language that gets concurrency right.

Lol. I guess you haven't seen Elixir.


Go's approach to concurrency exposes Go's lack of memory safety.

When you couple this with a (mostly) bunch of noob devs who have been told Go is super safe because of it's type safety, you have a recipe for disaster.

The combination of language features and surrounding social effects means, once you pursue concurrency, Go abruptly becomes one of the most dangerous languages to use for any industry where subtle errors of value are a problem ... like anything to do with money.

p.s. No slight intended to Elixir, it avoids all these risks.


Don’t know if use tools like static check but Go’s type system kinda suck without development tools


Async in the nodejs ecosystem seems to be doing perfectly fine?


async (the keyword) is doing fantastic because it’s better than all of the available alternatives. Those alternatives:

- ad hoc callbacks, which had a great Result-ish type signature but really do warrant the “hell” in “callback hell”

- Promise APIs, which are semantically equivalent to async the keyword, unless you care about call stacks, and have a lot of the same hellish problems as the ad hoc callbacks they were meant to address (less nesting! same everything else!)

- Um fibers? Good luck making sense out of whatever that’s doing. It’s a good idea, but it’s also all opaque magic when you try using it.

- Actual threads and child processes… there are valid use cases, and they’re worth pursuing if you have a valid use case, but the facilities for development with them are basically “here’s a bunch of low level concepts that closely mirror their system level counterparts, hope you know/figure out what you’re doing!”


By async I was referring to node's asynchronous event-driven runtime abstraction which the GP refers to as an ugly hack. I'm not sure if this abstraction is better than all of the available alternatives if you compare it to high-level features multi-threaded runtimes offer like thread-safe collections, atomic updates, concurrent hash maps, immutability, structured concurrency, etc... as in Java/Clojure. Most Java programmers don't work with the low-level thread primitives.

The author of esbuild gave up trying to code esbuild with nodejs/worker threads and switched to Go for a less limited/restricted concurrency environment.


> By async I was referring to node's asynchronous event-driven runtime abstraction which the GP refers to as an ugly hack.

Okay with that clarification I can agree it’s a good model, given its constraints. The event loop with async IO in the abstract is a good way to model a single process/thread workload for many use cases that fit it.

> I'm not sure if this abstraction is better than all of the available alternatives if you compare it to high-level features multi-threaded runtimes offer like thread-safe collections, atomic updates, concurrent hash maps, immutability, structured concurrency, etc... as in Java/Clojure.

Clojure’s solution to concurrency is a breath of fresh air, regardless of your execution environment, because its state transactional semantics are great whether your concurrency is in one process/thread or spread across many. I can’t speak to typical Java solutions, but my general sense is they’re higher level and more powerful than Node’s for actually crossing process/thread boundaries, but subject to most of the shared state problems Java has even in a single process/thread.

> The author of esbuild gave up trying to code esbuild with nodejs/worker threads and switched to Go for a less limited/restricted concurrency environment.

After a lot of exploration of Node worker threads, I’d probably similarly look elsewhere if I had a workload suited to it. You can do a lot with Node worker threads with a lot of special tuning for a use case, and I even have some proof of concept code demonstrating that it can be much better than common usage. But I put it on hold because the complexity of making it perform well is very high compared to optimizing the single thread default.


What is there besides Task<T>?


First there was APM https://learn.microsoft.com/en-us/dotnet/standard/asynchrono... then there came TPL (using Task but without async await but ContinueWith) and finally async await with some later refinements like ValueTask. Generally you just use async await nowadays with maybe the complication of when to use Task or ValueTask


C# tends to have this problem with multiple ways to do the same thing. Look at construction:

    var a = new A();
    A a = new();
    A a = new A();


When did that second one get added?

I haven't written C# in a while, but I don't remember seeing that one. I skimmed the C# grammar and didn't see it there either, but I only glanced and might have missed an entire section.


C# 9.0: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/cs...

I'm honestly surprised they haven't add the ability to drop the `new` keyword like Kotlin:

   val a = A();


Thanks! For anyone else who’s curious, these seem to be called “Target-typed new expressions”.


It is a nice language. I just wish they stopped adding features. Except for the one everyone wants, discriminated unions.


I find many new features useful as they usually either reduce boilerplate, increase expressiveness or enable high performance code. Some are also related to AOT compilation.

Which new features do you dislike?


C# does seem like its popular with quite a few of the crowd here. Not very popular in startups though.


Agreed. No one should think that there will be one language for everything and everyone.


Or Go, whose guiding principle seems to be "Pretty OK"


Go guiding principle is rather “the lowest common denominator”.


Python doesnt really scale to large code bases, and refactoring is very hard.


Do you have any real world experience? I'm working on a Python codebase which is almost 2 million lines of code with almost 100 developers and I haven't seen development speed slow down over the years.

Of course you need to have a structure within this codebase or you'll bump into the typical problems of large codebases. Some languages force you to work this way, we do it by enforcing certain rules in the lower level framework code of the project.

Also you need to keep pace, we are on the latest Python version and code is heavily typed. This project was started in the 2.7 days.


I’ve got a long experience with Ruby which I believe is quite similar, but after about 5 years, an untyped language just falls apart. Updating anything is perilous because libraries will change stuff all the time that violates the types and it won’t be mentioned in the changelogs, your 20,000 unit tests will also constantly miss stuff because you didn’t reimplement a type checker inside rspec. It’s a disaster. Typescript has saved our front end but I don’t see any hope for Rails.


typed Python is becoming more common, and it helps. It’s not as mature as Typescript, but it’s getting better


It definitely goes a long way towards making things more workable/understandable.


Have you tried Sorbet? The typing story in Python and Ruby are both pretty bad, but they do exist...


We investigated but the general vibe is that it just isn’t there yet, and it isn’t even moving in the direction to get there. It really needs support from the Rails project, and the rails leadership had indicated it isn’t going to happen.

If you have a pure ruby project and use it from day one, it probably works. But if you are starting a brand new project, just save yourself the pain and use TS.


I love this comment so much.

> Of course you need to have a structure within this codebase or you'll bump into the typical problems of large codebases.

> Also you need to keep pace

I don't have the wherewithal to really dig into this now but to both those points I respond with a loving and resounding: "Duh!" (not directed at you)

What sort of project is it?


Are you allowed to say what project/product it is? I'm curious. Do you use type annotations? I've found refactoring sooo much easier in statically typed languages like Go than in Python, even for medium sized projects.


Most code has type annotations but we don't enforce it. A lot of code was written before typing became well supported and we never went back to add it to every old piece of code, we add them when people touch related code sections.

Typing definitely makes it a lot easier to navigate the codebase. But in the end maintaining any large codebase is a question of discipline which starts with simple things like code layout, class and function names, isolation of functionality.


> I haven't seen development speed slow down over the years.

Well, otherwise, how would it have gotten to 2 million lines of code?

Adding lines of code and refactoring are different activities and discussions.


I wanted to say that the per-developer output has not slowed down significantly.

But I admit that there are challenges. For example startup time is ~30 seconds because that's the time it takes to import everything. This automatically means that running even a single test will take at least 30 seconds, to not bang your head against the wall we mostly write new code in a REPL so everything is already loaded.


Instagram and Dropbox would like a word :)

Also it's hard to beat Django in terms of productivity. You can use type defs too.


Worked on Instagram codebase for almost 2.5 years, never wrote a single line of Python, most of the services are hack or c++. Where does python come into picture? Apps are in java/kotlin/ObjectiveC/Swift.



Not if you use modern python. And by modern python I mean typed python. Heavily typed.


Huh? I've always used punch cards for my python...


matching the indentation level sure is easier with punchcards


assembly/c/c++/perl/ruby/elixir/elm/js are "pretty ok" to me no matter how long I had written with. python is absolutely not ok to me .. always think "really?" I want to write it in another way, but it can't be.


What soured Python for you?


Not OP but to me the ecosystem is too disjoint. To ship "production python", you need to figure out which of the "current" tools are to cobble together to build, test, and share your code.

`cargo` combines poetry, ruff, pipx, sphinx, pytest, etc. You might expect this to be worse. In some cases, it is (e.g. pytest). The super power is in being able to get a project up and going immediately without wasting time on figuring out which tools to use, scaffolding them together, setting up a host for your documentation, etc.

Then there comes the problem of distributing it where the target system has to have just the right version of Python.


My experience with Python recently:

I have been working on a mediumish project (without tests or type hints) where I needed to add functionality including a bit rewiring.

My main problems were:

- I didn't know what data (types) were passed around when looking at parts of the code, so I had to look were the data structures were created, adapted and passed along to actually know more (or just debug it)

- After changing parts of the code, I was 100% certain that I had to run it a dozen times to fix new bugs or things I had forgotten

How it works with Rust:

- I see the types in the function signature

- I know the properties of 'classes' without scanning the whole code of the class

- when it compiles, it works 90+% of the time, because the compiler tells me where I forgot something before, if the IDE hasn't already told me


> I didn't know what data (types) were passed around when looking at parts of the code

For me this is the big one. Types isn't just about writing error-free code, it also aids in making the code self-documenting.

When I read a function signature in an unknown codebase in C, C++, C#, Delphi or similar, I can almost always tell what it does and what I can do (or not). Even types I haven't seen before helps, because the name of the types can be informative.

Thanks to this I've helped countless people fix their code using libraries I've never seen before in mere minutes, simply by glossing over some of the code.

When I work in untyped code, like older Python or similar, it's much harder. Invariably I end up sprinkling dir() or similar just to figure out what the heck is going on. Ok, this so function created... something... what can do with it? Who knows?


Python also has the regrettable property that many type errors aren’t even caught at runtime. Instead you often get incorrect results with no exception.

str vs bytes confusion is a major example. If you use one where you needed the other, the results can be anywhere from inscrutable failures to hilarious. My personal favorite is if you stick bytes into a templating engine. For some reason, iterating bytes in Python yields integers (not bytes-of-length-1 or a separate byte type), and those integers can get stringized and flattened, and your webpage turns into a bunch of digits and you end up with the Matrix.


> How it works with Rust ...

You will be amazed to find that that's how it works with C, Pascal, Java ...

Types being declared in function headers and checked is 1960's technology.


But Rust puts a unique amount of information into the types. This can get annoying sometimes, but it means typechecking better predicts correctness.

For example, a c header type might have an out parameter and an error code returned. The interaction between those two isn't specified in the type system.

I personally require discriminated unions (i.e. enums where each variant can hold its own type of data) before I consider a type system adequate.


But that's way beyond someone coming from JS or Python, just being excited that there are types on the parameters and the compiler tells you when basic things are wrong.


I personally came from JS to Java and got annoyed I had to do all this work specifying types but they gave me essentially no benefit.

Then I tried Rust and realized why types were good. (and then Kotlin)


it’s not just the type checker; its the borrow checker, which prevents way more bugs than in C++ and is certainly not 1960s tech


certainly not with C:

Manually managing memory has turned out to be error prone (for me) and can result in quite nasty to debug bugs (because the type system is lacking), only to mention the most important thing.

I think there is enough out there on C vs Rust, esp. because of the Linux kernel

That said, it was asked about Python (within the context of Rust), and I have limited experience with Pascal and Java, so I will leave it at that.


except the type system in C isn't that good.


That’s an understatement. If you see a function in a header like:

    char *func(char *in, size_t len);
It’s anybody’s guess what the actual input type is, not to mention the ownership or lifetime of the output.


HN now supports backslash escapes for asterisks (which, unescaped, are markup notation for italics).


> I didn't know what data (types) were passed around when looking at parts of the code, so I had to look were the data structures were created, adapted and passed along to actually know more (or just debug it)

It's even better when all those data structures are just dicts, because it's too cumbersome to make serializable classes, so good luck figuring out what fields are in which.


Not Op but once I experienced Go I started writing less Python by choice. Then TypeScript and hopefully soon more Rust.

Typed Python 3 is just awkward enough to not make me enjoy it on small scripts and not accurate enough (yet) to be confident in it on large projects.

I don't use numpy & friends so Python doesn't offer me much any more compared to newer typed languages once I go beyond a simple script.


You should try out deno. TypeScript with zero set up. You can write a .ts file anywhere on your computer and run it with, e.g., `deno run hello-world.ts`. It's my new goto for quick scripts


Yea! I actually wanted to teach using Deno back in 2019! It was a stretch topic for the last couple weeks of class in my syllabus but we were too far behind.

Excited to see where it continues to go and we are writing more scripts in TS at work (without Deno, tho).


> Then TypeScript

Have you done any backend TS? I'm curious what the best practice there is, these days.


I am definitely not the person to ask there.

I have been on a couple teams that used Node. Everyone was/is still using express and various ORMs / query builders. Knex and Prisma are what I saw.

Lots of neat stuff with Zod and relatedly EdgeDB.


One too many TypeError at runtime. I became increasingly annoyed that nothing was catching my silly typos or other mistakes. These days there are some really good python static checkers available, but I'm not sure I really knew about them half a decade ago when I started to move away from Python


can you recommend a checker?


I left Python when I found Scala, and while a good type system was a big factor, the groundhog day of "Python dependency management is awful with 4 different tools none of which quite work right, but this new tool has fixed it, for real this time" every few years was the part that really made me despair.


I don't know if I just use Python "wrong" or something. When I worked as a TA, I've written a lot of untyped Python as Blender/Houdini plugins, and I think it worked pretty well for this purpose.

But later I did some backend stuff, I tried to use typed Python and wrote safer code. Then the whole toolchain becomes so cluncky. MyPy, Pylinter, Flake8, etc. They're so slow, and their VSCode extensions are so weird to configure. And I never got the best practice to work with other untyped libraries. It's not that I don't know how to use typed language. Actually I do backend stuff faster in Rust than Python.

It's probably just me or my VSCode acting up.


it’s slow and there’s no helpful compiler


In my opinion it's not pretty ok. It's actually superior to many other languages.

I think for web stuff though the borrowing and move semantics is a bit of an overkill. Usually you don't need to thread or handle state in these areas (frameworks and databases handle it for you) so a GC works better here. But everything outside of that is superior to something like ruby, python or go when it comes to safety.


Matklad has hinted at this before, but it's funny how Rust is displacing ML dialects in domains where manual memory management is not at all necessary, simply because no ML has its tooling and deployment story straight.


I’m going to sound like a broken record, but F# is in the ML family and has wonderful tooling and the deployment story is getting dramatically better recently (it’s a little behind C# in its true native AoT compilation support, but it’s coming)


How do you build F#? I had a pretty bad time with FAKE/Paket the last time I tried it.


Honestly, the built in tooling has been good enough for me, in the .NET Core era (especially within the last few years). The MSFT CLI/IDE team has finally had the chance to fix some previous really annoying issues and limitations in the last few years, so the experience has gotten quite a bit smoother.

I know people that looove Paket, especially in the F# community, but I’ve been happy enough with things now to not feel like I need to invest the effort in adopting it.


I think if Rust could integrate a GC pointer elegantly into the ecosystem and put some of the knobs for running the GC the responsibility of user-space libs (ala pluggable GC), add in more optional type inference in places, and make it easy to refactor code to remove the GC, then it could become quite interesting for higher level glue code. As is, it’s not quite as good as TypeScript I think


There is a reference counting GC in rust. It's just not default and you have to be explicit about invoking it. It's similar to shared pointers in C++. See Arc.

For something like python the heap allocation or stack allocation is handled behind the scenes so the entire concept is abstracted away from you.


I meant a tracing GC like https://docs.rs/gc/latest/gc/.

Arc still requires you to reason about ownership as does this GC.

I think single threaded async w/ easy no copy message passing is more akin to what most people want when thinking about concurrency (eg Go channels) and actually happens to perform exceedingly well when it’s done up and down the stack (eg io_uring). In such a model you don’t even need to worry about ownership so much because you can’t Send anything except for things that need to be so there’s not a lot of value in many of the protections Rust tries to put in place.


It's hard to overestimate how extremely Rust is optimized towards making refactors as easy as possible. This often means a greater cost for initially writing some code, but once that code exists, it's way easier to search around in that code and also way easier to refactor it. Often times, being written is only the start of some piece of code's journey through the years and decades until it is finally deleted.


Editors like VS code just become super powered when using Rust too. Switching back to Ruby feels like crossing a road with my eyes closed in comparison.


+1. Started a new project targeting WASM and decided to learn Rust coming from a Python (hobbyist) background. The VS code experience is like I stepped into a time machine and landed in the future. Oh my god, it is so much better. I don't think I'm going back to Python unless I have to


Have you tried any other typed language? I think going from hobbyist Python to any typed language might have that effect


Tried F#, beautiful language. Read some tutorials and was amazed. Hated getting setup and VS Code felt clunky in comparison.

Tried TypeScript but that felt.. like a dirty afterthought. Maybe just because I generally dislike JavaScript? I am still using both for this project (since WASM+React) but can't say I love it

Rust was just like seeing God.

The Rust book is awesome. Well written, fun, comprehensive. Reminded me of my experience with the Django Book way back when...

Namespaced module imports? Yes, please.

Then cargo "Just Works"! Especially compared to what I had in Python (particularly if you compare to my first days using the language!) or the messiness of Javascript...

When I learned I could use cargo to open docs for a crate or build my own documentation in section I nearly fell off my chair laughing with joy


books for peeps contemplating the big leap:

Besides THE book (Klabnik/Nichols), the ones from O'Reilly, Pragmatic and Nostarch (for Rustaceans, Gjengset, in particular as a 2nd book) are excellent to superb. I might have missed a few, these were the ones from 3 public library systems.

Just look for 2nd eds on THE book and Oreilly's Blandy et al. Course i saw one from Wrox that was a pass.


thank you


Love that. Thanks for sharing your experience!


Have you tried Django with types? Refactors aren't as safe as with rust but pretty good.


I haven't and maybe I should, thanks for the tip. I love Django (and have proudly contributed a handful lines of code!) but unless I'm doing simple CRUD type admin-like apps, I probably won't go there again these days.


I get it. Most of my stuff is CRUD one way or another though so I'm quite enjoying Django :p

Getting the admin UI for free is nice.


Why? Give some examples. As someone with little Rust experience, it appears similar to C++, which I have a lot of experience with. C++ is a chore to refactor.


For a C++ -> Rust comparison, I think the best examples would be changing return types. Imagine in C++ if you wanted to change a `Foo*` to a `unique_ptr<Foo>`.

All the rules about when you can safely use/share this thing and who needs to deallocate this thing becomes a pain to figure out throughout the code base.

With rust, because ownership is baked into the type system, those classes of problems just don't exist. Change it from a `Foo&` to a `Box<Foo>`, no problem. The compiler will blow up and tell you exactly where (and how) to fix those things.

This applies to more than just memory allocations. Imagine calling a method from 2 threads that manipulates shared memory. Rust won't allow you to do that without protecting that memory with something like a `Mutex`.


It's the type system. When people say a language is easier to refactor it's usually because the types are designed more like ML, with a little more formal rigor, so the compiler errors can actually help you along as you make changes. Rust's model of mutability adds context to what you're doing - which is also what makes it hard at first(since it adds ceremony and constricts your initial design). In a large system where data can move through a lengthy code path with a lot of indirection, more nuance in types becomes a major advantage to your confidence in making changes.

C++ can never quite get there despite all the innovations made because of its descent from C style types, which are much more simplistic.


It comes down to the type system. In C++ maximal use of the type system is optional. If your C++ code base is C with classes, refactoring will be a mess. If your code base uses modern C++ with maximum type safety, refactoring is straightforward in the same way it is for Rust. The experience in C++ depends on the code base.

Use of maximal type safety in modern C++ is astonishingly robust and expressive. If it compiles it generally works. The majority of developers are a bit lazy about type safety in my experience, which works against you. It depends on the org.


In one sense it's impressive Rust has mediocre refactoring support despite orders of magnitude less investment in tooling. But that isn't worth all that much. It's no worse than any other typed language I've tried.

Rust in theory has a lot of potential here because it's designed to minimize action at a distance.


In addition to what others have said, C and C++ rely on a textual macro preprocessor and text-based #include, which can lead to a lot of annoying code-breakage when refactoring. Automated tools can't parse C/C++ unless they're effectively built on top of a full compiler; even building a complete C preprocessor is an extremely non-trivial task.

(Many newer C++ code-bases discourage relying on the preprocessor, but that only goes so far.)


It's not that it isn't tedious, but the workflow is a lot more reliable. Refactor, `cargo check`, fix the errors, repeat. When there are no more errors, it is very likely the code will still work.

I would say C++ is mostly the same experience, with the exception of anything involving memory safety & lifetimes. It can't guarantee you didn't accidentally introduce a use-after-free/move or anything else of that nature.


That's a huge benefit though - I'm more than happy to slowly and methodically refactor a system if at the end of it I can be nearly 100% certain that I haven't inadvertently broken something in the process. The time spent upfront is multiplied time saved in fixing bugs in production.

In a tortoise and hare race, sprinting off quickly doesn't count if you can't reliably reach the finish line.


It's hard to give concrete examples because the problem is inherent to large, "real" codebases - toy examples can't really do it justice.

That said, there are some general themes:

- The lack of implicit magic in the language. Destructors (specifically the fact that they are called automatically) is implicit. That's it.

- The context-free nature of the language. This exists at several levels:

    1. At a function level. There is no cross-function inference and all the information about a function is in its signature, so you don't need to look anywhere else to understand what it does.

    2. At a module level. Everything you use from elsewhere is explicitly imported in the module. There's nothing that can change what a piece of code does across a crate (eg. the way you can just redefine `true` to something else in C/C++.).

    3. At a file level. The module hierarchy is explicit, it is not inferred based on the layout of files on disk.

    Together, these things means you can look at a piece of Rust code and understand it in isolation, without even needing to be familiar with the codebase as a whole. You can move a piece of code from one location to another and (aside from fixing imports) can expect it to continue to function exactly as it did before.
- The amount of information that is available to the compiler. The rich type system and lifetimes means that it's harder to mess up a refactor without the compiler detecting it.

- The fact that it can be used as a safe language. This is not unique to Rust, but Rust is unique in being suited to the lowest-level tasks whilst still being safe. Safety limits the damage a bad refactoring can do (at worst, you break the code being refactored, but at least you won't give the whole program UB or cause a segfault).

- The fact that macros operate on the AST rather than text. This eliminates a class of bugs with C-like macros, where implementation details of the macro have a tendency to leak through the macro definition. Refactorings can often trigger these kinds of bugs because you cannot easily think at that level while doing a large refactor.

- The tooling. Whether it's quickly knowing the type of a variable, swapping out a dependency, or applying broad fixes to a codebase, the tooling works extremely well together.

- Consistency of the language. Everything is an expression. "void" is just a normal type, etc. This level of consistency means that large refactorings require fewer actual changes than they might do in other languages. For example, when I used a bit of C# again after using Rust, it was very noticeable the amount of "pointless" transformations I had to do when refactoring because making an expression conditional may require introducing a new variable because `if` statements cannot be used in an expression. (Yeah there's the ternary operator, but it can't be used in every case, and that's just another example of inconsistency).


> The lack of implicit magic in the language. Destructors (specifically the fact that they are called automatically) is implicit. That's it.

Well, destructors and unwinding. Many, many bugs in unsafe code have been found through unwinding code paths being not fully accounted for. At least in safe code that doesn't use catch_unwind, the worst that can happen is an unexpected state of an object shared across threads.

What's even more fun is the combination of the two. For instance, the catch_unwind function has a big flaw, where it returns an arbitrary Box<dyn Any> to represent the panic payload, and if you aren't very careful about dropping that object, its destructor can panic and start unwinding again. (There are plans to fix this by putting the payload in a shim type that makes the destructor abort on unwind, without actually changing its type_id. I find it quite an ugly hack, but there's not really any other way around it.)

(There are also other ideas to remove language support for unwinding destructors entirely. I'm against that for a few different reasons, but I'm not the one running the show.)


This is more or less the answer to the question that I'd have given sfpotter as well. The point 1. is the golden rule that the blog post talks about.


The author talks about http servers. So I will throw a question here.

I am a gopher that wanted to try rust, and I looked into http servers in rust, and it looks like there is like 10 different http servers, and each of them carries with them something that is called “async runtime”, and there are different async runtimes, and you cannot (or can?) use them together, and I realized async rust is not yet finished (or it is?), and I got kind of lost.

What http servers people used? Do the asynch runtimes matter? If I want to “just” build a http server, do I need to study different async runtimes?


Pretty much all the http servers of worth, short of tide, just use Tokio at this point.

For the most part, you really only need to care about Tokio. async-rs is used by some libraries, but it's relatively small in comparison - and many now go to lengths to just support both. There are other runtimes you could tinker with, but really: just use Tokio.

The "easiest" web server/framework to get started with in 2023 is, in my opinion, Axum. It's from the same people who work on Tokio and it does make it somewhat straightforward to dive into as a result.

I have a few actix-web servers in production as well, and it's a fine framework - hard to say where the mindshare falls on it these days.

Rocket is a very nice web framework but effectively has a bus factor of 1 at this point and it's started to show. People coming from e.g Rails may appreciate it though.

Don't use warp (IMO) because http routing is not rocket science and doesn't need the amount of magic it tries to bolt on.

Have fun.


I mean, there's only one http server people actually use, which is hyper. Just like in go where everyone uses net/http, even though fasthttp and so on exist and everyone just ignores them.

Just like in go, there's also a lot of web frameworks built on top of it (like all the ones listed here for go: https://github.com/mingrammer/go-web-framework-stars)

Just like in go, you can choose not to use any if you want and use hyper directly, or use one that is a quite thin layer on top of hyper like axum.

In terms of async runtimes, just pick tokio and get on with your life. It's the default choice, so picking it is the simplest option.


Async is really painful and a "not finished yet" thing.

However, in practice, if you use Tokio you should be fine. And usually this decision will be made for you with the framework you pick.

My #1 tip is to decide on exactly what your use case is, then use cargo and look at the recent number of downloads and go with the most popular framework. Doing this for several tags (http, server, framework) is required for categories that are noisy or not tagged well.

Example: Are you writing a web service API, a web server that has server-side templates, a client-side SPA app, or a server-less function?

Usually you will see they are all aligning on the same stack tokio, serde, etc but this is definitely something to be aware of. It is critical that an application uses the same version of dependencies for cross cutting concerns.


> async rust is not yet finished (or it is?)

https://areweasyncyet.rs/

see also: https://www.arewewebyet.org/


My (older) code uses nickel, a sync web server that is, sadly, out of date.

I'll be migrating from that to one of:

* https://github.com/tomaka/rouille

* https://github.com/tiny-http/tiny-http


Yes, you must deal with async

It's not that big of a deal though

Axum is popular but feels too low level to me, but some nice porcelain APIs might eventually be put on top of it


It's possible to avoid async contamination in Rust. Game-type programs generally don't use it, because they're compute-bound. The use case for async is a huge number of mostly inactive network connections.


What makes it feel low level to you? Axum has been my favorite way to do HTTP servers for some time. Especially where it is now, everything feels expressive, and I can do whatever I need to. If you're just doing basic stuff it makes it easy and if you're going beyond that it makes it possible.

There are a few parts of the learning curve that come up but the official examples showcase it all beautifully and the community is very responsive if you ask for help.


What feels higher level to you than Axum? I picked Axum because it seemed higher level than the alternatives I looked at, including actix and warp.


They're likely talking about Rocket which is higher level than Axum or Actix Web, but it's recommended not to use it as it's not as actively maintained, as well as some other issues. For a Rails-like experience, Nails [0] is one I've seen, although I don't know the status of it as no one seems to talk about it as much as Axum and Actix Web.

[0] https://rust-on-nails.com/


I think axum is pretty decent tbh, considering it's part of Tokio and will get better support than most servers out there


cargo and rustc need work:

- sccache helps with some parts of builds but rustc needs to be caching a whole lot more for rapid TDD iterations. It shouldn't take seconds or many minutes to build things. (Invoke "go does it in milliseconds" card.)

- cargo should migrate away from git and sparse protocols. Both are slow because one abuses git and the other fetches each crate metadata as a file using individual requests. It would be faster and simpler to send either a total compressed sqlite database OR a compressed SQL transaction to migrate between versions. N files on disk representing each crate is asking for fs performance problems in the real world on fses people actual use.


I've found the sparse protocol many times faster than git since Rust 1.68

To use the sparse protocol with crates.io, set the environment variable CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse, or edit your .cargo/config.toml file to add:

  [registries.crates-io]
  protocol = "sparse"
https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html#cargo...


On the topic of vertical scalability, I have found that many runtimes require something else to scale. Ruby requires passenger/unicorn/puma/etc. It requires pgbouncer if you're using postgres. It requires redis for background work. Similar situation with Python, or node. You need a process manager to handle forking for concurrency, you need external connection pools etc.

I can see and appreciate the unix philosophy at work, but it's also adding complexity. More things to tune, more things that can go wrong or become a bottleneck.

As we've moved our services to Rust, we've found that all those special tools disappear. Sure, we need to make similar decisions around how our system runs, but (as an example) configuring an in-process db pool is much much simpler than pgbouncer.

And that's a big part of a runtime being scalable. We won't be forced to reach for another tool as our requirements expand. We still might, but we don't have to.

(Certainly golang and the JVM fall into this category as well)


I feel like programming language designers should see themselves as quite beleaguered at the moment.

Even prior to full AI replacement, a properly trained AI will be able to take English language and effectively translate it to machine code, assembly code, or C code.

It doesn't really make much sense to have the human at the this layer for much longer.


> Rust is so easy to compose reliably that even the stdlib itself does not shy from pulling dependencies from crates.io.

The standard library depends on external crates?

(Edit to remove that last part since it seems irrelevant.)


> The standard library depends on external crates?

Yes: https://github.com/rust-lang/rust/blob/478cbb42b730ba4739351...

Of course it is not "some random" crate; all of them is either in the same repository (when `path` is used) or in other repositories under the `rust-lang` organization with well-known contributors. But the latter is conceptually no more special than other repositories, so Cargo works same.


Thanks! I was interpreting the comment to mean code that wasn’t in the standard library repository, which surprised me.


You interpreted it right.

There are dependencies on other repositories in the rust-lang GitHub organization. The optional dependencies for the backtrace feature include libraries outside the rust-lang GitHub org.

https://news.ycombinator.com/item?id=35350600


Thanks! I did see your helpful comment, but I only replied to the other one :)


I just peaked at the crates.io dependencies in the standard library.

There's three dependencies to high-quality libraries in the rust-lang GitHub organization: a macro to make conditional compilation easier (cfg_if), a port of Google's SwissTable hashmap library (hashbrown), and bindings to libc. These all have maintainers in common with the standard library.

The rust standard library hashmap is (zero cost) wrapper around a hashbrown hashmap with a subset of hashbrown's methods.

cfg_if is probably used just for the convenience. I guess they're not ready to commit to stabilizing that macro in the standard library for some reason.

libc was originally autogenerated from the c header files, but if I recall correctly updating it involves a bunch of annoying manual processes necessary to ensure perfect compatibility. It's obvious used in the implementation of standard library features.

Then there's a handful of dependenencies for the optional but enabled-by-default backtrace feature like a symbol demangler and a zlib encoder/decoder. I'm not familiar with their quality, but I don't think it's as excellent as the other dependencies.


Rust is SCALAble :) #dadjoke




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: