2 years into Rust, Impressions

Hello, community,
in summer, I posted reflections on one and a half years into Rust. Now I have new vision.

Since then, I noticed there wasn't pretty much any difficulty. But then I had to use a new, unfamiliar package, and it was painful. The APIs are complex, sometimes because Rust has no function/method overloading like C, nor optional arguments like Python.

So authors either make several interfaces, or extra structs like "options builder", or resort to macros. Or do all of these. And getting a new package to work, beyond the hello-world examples in the docs, becomes very hard and frustrating.

For instance, I needed a simple HTTP wrapper around an API, and decided to take axum. From the docs, I could not find out how to make fallible method for it. So I tried reading the sources, to see what the get function does (it works like get(my_function), ...but it's like this:

top_level_handler_fn!(delete, DELETE);
top_level_handler_fn!(get, GET);
top_level_handler_fn!(head, HEAD);
top_level_handler_fn!(options, OPTIONS);
top_level_handler_fn!(patch, PATCH);
top_level_handler_fn!(post, POST);
top_level_handler_fn!(put, PUT);
top_level_handler_fn!(trace, TRACE);

Names aren't found in the sources -- you should expand macros to see what it turns into.

Again, all I needed was a simple API method that runs, but has multiple error points that had to be caught -- but that required getting familiar with a good portion of the library, or asking trivial questions in the forum. Sometimes, you need the whole knowledge just once, and learning the library internals to get it working in a slightly more complex way seems too much.

Apart from that, when you're not needing new 3rd party code, things are intuitive.

2 Likes

I see how much the Rust echo system relies on macros with a bit of worry, tbh, Its such a leap in complexity from regular Rust to just have it everywhere, I wonder if they were ever intended to be used in the way they are today.

Comptime should really be a priority for Rust in my opinion, I'm a bit sad it didn't even come up on the 2023 State of Rust Survey.

That being said, I'm curious about why you needed to look into the Axum internals to do this.

2 Likes

This is a problem for std as well. Macros and the [src] hyperlink interact really badly.

3 Likes

I was struggling trying to merge two examples, a simple and an advanced one, but it didn't work (error messages were not helping, even though I think they're good usually). So, I tried to do as in Python, to read the sources and get some clues of what it wants from me.

Isn't this more about the thing that axum wants to do is just inherently difficult? They provide a very general-purpose router for HTTP, a protocol which has significant hidden complexity itself. It would be easy for them to offer a router with just a single very simple trait that has methods like get, put, post, delete, and call it a day.

But that's not the value proposition for the library. They want to give callers the flexibility to pass things like global state into and out of handlers. Access cookies, headers, query parameters, path components, etc. without sticking to a static interface. And that comes with some complexity baggage.

warp and tide also do something similar, and also have a lot of inescapable complexity because of it. On the other hand, ratpack is much more of the "static interface" flavor, but they give you a macro to paper over some of the boilerplate that compositions require.

Your decision to use axum isn't necessarily bad. It just probably has a lot more bells and whistles than you will ever be able to reasonably use, but you still have to pay for the cost of those things even if you don't use them. You could have chosen a different crate with a simpler interface. Or built your own to handle only the parts of HTTP necessary to your specific domain.

4 Likes

The opaqueness from macros is indeed a problem. This can probably be mitigated by macro expansion feature integrated into development tools.
But personally, I think that to use a library well, you do need to understand its internal implementation, just as to use the Rust language well, you need to look through the std source code often.

Probably you're right, but I had similar experience when needed to use another 3rd party and an unfamiliar std module. In both cases, I suddenly got reminded how hard it was earlier.

I just read some reviews of the APIs of 3 http frameworks, and axum had the necessary features, which I fail to remember.

Nope. I have done things that axum does with macros in C++ with templates. With if constexpr that's easy. Just go with auto/auto/auto and then look on types with various checks like std::is_same_v.

And, of course, comptime in Zig makes such things even simpler.

But Rust designers have decided that post-monomorphisation errors are unaccessible and we have to use macros instead. This made traits “simple” but now we have to use macros to generate insane amount of code which wouldn't be ever used.

It's as simple as that.

Unfortunately “simple interface” means manually-implemented dynamic dispatch, more-or-less.

If you want to do things at compile-time then Rust only offers you macros, more-or-less.

You may play tricks with generic and post-monomorphisation errors in some cases because const are processed that way, but they are limited and that's why we have all these macros.

Obviously the decision to push people to use macros instead of templates was explicit, but I'm not 100% sure Rust developers understand how much this decision affects the ecosystem.

I, for one, strongly suspect that these “why is Rust compiler so slow?” cries may be traced to that decision: Rust compiler is actually faster than C++ compiler in apples-to-apples comparison, but because of the “no errors post-monomorphisation” stance many Rust crates are generating 10 or 100 times more code than equivalent C++ libraries… of course at that point Rust compiler becomes rather slow.