eknkc
6 days ago
While the implementation is straightforward enough, this post clearly shows me why I don’t like Rust.
So, the function accepts two values that implement Mid trait. And there is a blanket implementation for Mid that applies to anything can be added, substracted, divided etc and can be derived from a u8. So now this will make every numeric type a Mid. (Which, as the author states only an example I guess)
I can read and understand how this works here. But when I’m in a larger code base, I can not find what trait implementation made what possible.
With all the trait bounds and implicit conversions happening around and macros implementing traits on stuff it becomes a shit stew in a hurry.
And everyone always reaches to the most clever way to do things. I mean, does Mid need to be a trait? I guess it works beautifully yeah. I’d prefer to pass a midpoint calculator function instead.
I guess this turned into a rant. I actually really like Rust the language but I think I don’t like Rust codebases basically.
It is the least readable language I’ve seen not because of the syntax but because of all the cleverness going on around the traits and macros.
ChadNauseam
6 days ago
Having the function take a midpoint calculator is fine and there are some advantages to it (e.g. it makes it easy to turn into a linear search if you want to do that for some reason). However since writing a midpoint function isn’t always totally straightforward (an incorrectly implemented midpoint will cause correctness issues or even infinite loops), I think it’s nice to have it as a trait. That way there’s little risk of someone using a bad `|x,y| (x + y) / 2` implementation out of laziness.
However I 100% agree with you that traits make it really hard to figure out what actual function you’re calling. I wish there was a way to see something like “in your code, this generic function is called with types T1, T2, etc.” and see the trait implementations for those types as well. I’ve actually wanted to contribute this to rust-analyzer for a while.
aaronblohowiak
6 days ago
I have good news for you, it gets worse! Look at very large and old ruby codebases :)
Ygg2
5 days ago
Ruby and Rust have outside Turing completeness very little to do with each other.
steveklabnik
5 days ago
I think your parent’s point is something along the lines of “method lookup being complex doesn’t inherently limit the popularity of a programming language.”
inferiorhuman
6 days ago
I can not find what trait implementation made what possible.
That's what your IDE is for? If I pull up the code docs for e.g. a call to an iterator function I see which object or trait that function came from.ithkuil
5 days ago
Not only IDEs. Also generated documentation clearly states which traits a type implements and why
forrestthewoods
6 days ago
I like Rust a lot, but I strongly agree. “Advanced” Rust becomes as inscrutable as C++ template crap.
I really wish Rust had a way to define “template style” generic functions rather than “trait style”. Some things simply can not be done gracefully with trait spaghetti.
Ygg2
6 days ago
> I really wish Rust had a way to define “template style”
It has! It's called proc macro [1]. I'm only half joking.
[1] It can generate anything, errors generally suck ass in it as much in C++ templates and it feels similarly brittle.
> “Advanced” Rust becomes as inscrutable as C++ template crap.
I think people are abusing traits a bit too much. They are very nifty and often compile to very optimized code.
That said this trait is not an example of such abuse. Having a trait to help with overloading is the most benign form of trait use.
Solution to grandparent's problem is called rust analyzer. It will show you what methods are usable on your variable.
forrestthewoods
6 days ago
Rust macro hell is even worse than Rust trait spaghetti. I regularly refuse to use crates because they’re macro based and there’s a no macro alternative.
But you’re not wrong :)
tcfhgj
6 days ago
There's always a no-macro alternative: expanding the macro by hand.
What's macro spaghetti?
inferiorhuman
6 days ago
Certainly it's possible to generate meaningful error messages within proc macros. Most folks don't bother though.
pjmlp
4 days ago
I would assert it is actually worse than C++, because at least templates and constexpr execution still looks like plain C++, while macros in Rust, with two additional levels of syntax, add even more complexity.
LegionMammal978
6 days ago
Yeah, trait-heavy code is one of Rust's weaker points in practice. I find tower and some of the database crates to be some of the worst offenders: even knowing the language pretty well, I've often gotten lost in a maze of projections and blanket impls when an error occurs. There's no easy way to trace the implementation back to the intended behavior, to compare the expected and actual outcomes.
jms55
6 days ago
This has actually gotten better recently. You can now implement custom error diagnostics.
E.g. Bevy implements a trait (QueryData) for tuples of up to 32 items (A,) + (A, B) + (A, B, C)...
If you went over that 32 items, you used to get a confusing error about not your type not implementing QueryData.
Now you get a nice error message explaining the common reasons why this your type does not satisfy the trait.
unshavedyak
6 days ago
One of my annoyances is for something like `impl Foo for T: Deref<U>, U: Foo`. Ie a Foo impl for something that can deref to another type, which impls Foo. Eg an Arc, Box, etc. So now that you impl that to support, say, Box - well now if something fails to impl Foo, you'll instead get an error about it not being a Box (iirc, been a while). Ie, my memory is old so forgive me, but the error says something to the effect of your `T` type not being Box<T>`, instead of it simply saying that your T doesn't impl Foo.
I ran into this confusing error so many times i just stopped doing blanket deref impls and instead just impl it for `Box<T>` directly. Doing that avoids the weird error entirely.
Still, i love Rust. However it, like any lang, is not perfect. Nor should it be.
pimeys
6 days ago
Also tracing and rust-opentelemetry gives you a pretty wild ride if needing to do something more special with them.
worik
6 days ago
> I actually really like Rust the language but I think I don’t like Rust codebases basically.
And
> everyone always reaches to the most clever way to do things.
Yes oh yes
Rust was a brilliant idea spoilt by too much cleverness
I'm hoping for Rust II. A smaller language, with a bigger standard library and no dependence on run times.
By "no dependence on run times." I mean no async/await. It is an abomination in Rust
Conscat
6 days ago
There already exist many affine-typed languages with fewer features than Rust. When you phrase it like "Rust but with weaker type safety and higher-overhead abstractions" one might see why this premise fails to gain much traction, though.
aw1621107
6 days ago
> By "no dependence on run times." I mean no async/await. It is an abomination in Rust
Out of curiosity, what alternative would you have preferred to see, if any?
worik
6 days ago
Non blocking standard library functions
I find it astounding that so many people think asynchronous programming means async/await
Asyc/await is a way of letting programmers who do not want to learn how, do asynchronous programming.
It has poisoned the well
dwattttt
6 days ago
I very much do not want to go back to the days of "read up to 4 bytes off the wire to see how big the next chunk is. Oh I only read 3 bytes? Guess I'd better save those 3 + the fact I need to read 1 more, then drop back out to the event loop"
worik
5 days ago
> I very much do not want to go back to the days of ....
Not what I recommend. The C library does much better than that.
In asynchronous programming you can only deal with the data you have, so you buffer it. I have not counted bytes like that - for the purposes of reading from a file, in decades.
dwattttt
5 days ago
But that's where non-blocking leads; it can't block until it reads all the bytes you asked for, that has to be handled somehow.
A fancy C library could buffer the partial read for you rather than you needing to do it, and it could even maybe deal with turning your function into the state machine required to resume the function at the partial read.
But then you look around and realise you've created another async/await.
xedrac
6 days ago
I've been doing asynchronous programming for decades, and async/await is a notable improvement for many use cases. Event loop + callback hell, or condition variable/mutex/semaphore approaches are great in some cases, but they suck in others.
worik
5 days ago
> I've been doing asynchronous programming for decades, and async/await is a notable improvement for many use cases.
I think that goes to taste.
> Event loop + callback hell
That was always due to indisciplined programming. Async/await offers up other hells.
I think it makes some sense in memory managed systems. But in a system with static compile time management like Rust async/await forces all sorts of compromises. Mostly these fall on library authors, but it is its own hellscape. `Pin`?
> great in some cases, but they suck in others
That is a universal refrain for software systems!
aw1621107
6 days ago
> Non blocking standard library functions
What approach are you thinking of for the API of those functions, since async/await is not something you'd like to see?
mfenniak
6 days ago
I believe this is reference to Go's approach, where code can be written in a linear straightforward fashion with normal function calls, but, if they are syscalls then they become async automatically.
I don't think it's appropriate for the level of coding that Rust is targeting... But... It would be nice.
aw1621107
5 days ago
I suppose that makes sense given OP's definition of "no dependence on run times", but I think I'd have to agree that that approach probably wouldn't work well for the niche Rust is trying to target.
worik
5 days ago
> I believe this is reference to Go's approach,
I am unfamiliar with GO.
I am thinking of the way the C library does it.
> I don't think it's appropriate for the level of coding that Rust is targeting
That would be low level system programming. In part
aw1621107
5 days ago
> I am thinking of the way the C library does it.
What way is that? I didn't think the C standard library had any understanding of blocking/non-blocking.
worik
4 days ago
O_NONBLOCK
aw1621107
4 days ago
That's technically POSIX, not standard C, isn't it?
But in any case, I'm pretty sure Rust has supported the nonblocking functionality you want for quite a while now (maybe since 1.1.0)? You'll need to piece it together yourself and use the libc crate since it's a platform-specific thing, but it doesn't seem too horrendous. Ultimately it comes down to the fact that the fundamental method for the std:io::Read is
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
which is perfectly compatible with non-blocking reads. For example, std::io::File implements Read and std::os::fd::FromRawFd, so if you can get a non-blocking file descriptor you should have non-blocking reads for files.NoboruWataya
6 days ago
> It is the least readable language I’ve seen not because of the syntax but because of all the cleverness going on around the traits and macros.
Definitely agree about macros. A lot of crates use them extensively, but they are inherently less intuitive to reason about, tend to be less well documented and produce less helpful error messages when you use them incorrectly.
I think a lot of trait misuse probably stems from the fact that structs kind of look like classes and traits kind of look like interfaces, so people may try to do Java-style OOP with them.
(As an aside, the other thing that makes it difficult for me to grok Rust code is when everything is nested in 3+ levels of smart pointers.)