Hacker News new | ask | show | jobs
by Xeamek 756 days ago
Eh, Rust would be fine if not for the fact that it's too opinionated.

Unfortunately you can't just have Rust's safety checks, without opting into restrictions that Rust designers force onto You that aren't inherent to safety checks, but more because 'that's a better practice (according to us)'.

And also, easy and fast iteration just isn't there, both because of borrow checker restrictions and compile times

5 comments

C/C++ being non-opinionated is the main source of the security vulnerabilities.

Let's face it, it felt good to be a lone cowboy carrying a lot of responsibility and knowing what you are doing. I was there myself and I'll admit the ego trip was awesome.

These times are long past and naturally, people refuse to adapt.

> Unfortunately you can't just have Rust's safety checks, without opting into restrictions that Rust designers force onto You that aren't inherent to safety checks, but more because 'that's a better practice (according to us)'.

Show me something that does better and I'll switch tomorrow. But don't tell me C/C++ are better -- they are not. Too much freedom leads to CVEs literally every month somewhere and that's only because we don't have better vetting and checking tools, otherwise I'm sure we'd be getting one every day for a while.

> And also, easy and fast iteration just isn't there, both because of borrow checker restrictions and compile times

I agree on that, that's why I mentioned Golang. Most of the C/C++ systems I worked on around 15-20 years ago didn't need the close-to-the-metal speed because at least 90% of their time was spent on I/O... frak, even Python would have done well there. And Golang is times faster. It's a very nice compromise if you want to be productive and don't care super much about CPU speed efficiency.

> C/C++ being non-opinionated is the main source of the security vulnerabilities

This. "Undefined behavior" is such a terrible way of thinking. As is the "we can assume in the optimizer that UB does not happen and then eliminate code on that basis", which allows the compiler to introduce bugs that only appear at certain -O levels.

It took decades to get them to define arithmetic as twos-complement.

> It took decades to get them to define arithmetic as twos-complement.

I'm not sure this is right? IIRC C++20/C23 require two's complement representation for signed integers but generally leave other behaviors (including signed overflow) the same.

[0]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p09...

I’d say the biggest problem of interpreted, JIT compiled, and GCed languages is not the speed; it’s the RAM use. I agree that quite often we don’t need the speed of close-to-the-metal. But there is something wrong with most programs eating tens or hundreds of megabytes, without doing much.
And I agree on that. My favorite Elixir's runtime (Erlang's BEAM VM) still has some subtle problems with holding on to big(ger) binaries (strings) that requires very specific code be written at the green thread boundaries and it can get pretty maddening if you don't get it right -- which is not easy.

Golang I hear is doing much better btw. But you still have to be wary of its footguns i.e. leaking goroutines.

Go is doing much better on memory use because 1) it is AOT compiled, 2) it tries to allocate as much as possible on the stack (escape analysis), 3) it supports value types (the items of an array and the fields of a struct are contiguous in memory). But it's still a GCed language, which generally means a slightly higher memory use, controlled by $GOGC, to not spend too much time CPU time on garbage collection. Overall, Go is an excellent tradeoff for most applications.
There are restrictions out on You by the borrow checker to ensure safety, and then there are restrictions put on You by rust design team 'just because'.

Again, the former are fine, it's the latter I have a problem with.

In order for me to agree on the "just because" part you'll have to give some examples. What made you think they are arbitrary? And how did they prevent you from doing your job?
Comment above, mentioned borrowing while structs instead of borrowing memory. I believe this was once discussed under term 'partial borrows', but the "idiomatic" aproach is to 'just split your structs'.

Which isn't really a good aproach to structuring codebase, it's just to appeal to borrow checker inflexibility.

Lack of global scope. Lack of function overloading.

> Comment above, mentioned borrowing while structs instead of borrowing memory. I believe this was once discussed under term 'partial borrows', but the "idiomatic" aproach is to 'just split your structs'.

You're mistaking "idomatic because it's the only way" with "idomatic because we say say so". There is currently no way in Rust to specify the granularity of a borrow, so we're stuck with splitting your structs to get around it.

Part of the problem is that it's a hard problem to design around. It can not be done automatically by the compiler, because it would result in changes in the implementation being changes in the type signature. For example, say we have this function:

    pub fn foo(&self) -> i32 {
        self.a + 5
    }
The granularity is borrowing `a`. If we then change it to this:

    pub fn foo(&self) -> i32 {
        self.a + self.b
    }
The granularity of the borrow has changed in a backwards incompatible way (it now includes `b`), but that change is not reflected in the signature. It's the same reason why the compiler refuses to infer signature lifetimes from function bodies now.

You could, of course, say that we can allow the programmer to specify it manually:

    pub fn foo<'borrow>(&'borrow self) -> i32
        where 'borrow: 'self.a + 'self.b
    {
        self.a + self.b
    }
But this is now leaking implementation details if `a` or `b` are private fields.
That's truism, ofcourse there is no way to do something that is not impemented.

The question is whether or not the fact of it not being implemented comes from problem difficulty, or designers explicit refusal to do so

> Lack of function overloading.

Isn't there at least some technical basis for this (less-than-ideal interactions with type inference IIRC)?

I would say until C++98, C++ used to be more opinated, one of the reasons many of us went with C++ when given the choice, wasn't OOP features, rather the security improvements over bare bones C, with compiler provider frameworks.

Then eventually C++ got invaded by C expatriates, and writing C with C++ compiler idioms increased instead of going away.

It is like giving Typescript to groups of folks that insist on using any all over the place.

Actually I have good-ish memories of the early `boost` (we're talking 2003 - 2006) and some of the `std::` libraries in C++. They got the job done fine and were not in the way. So yeah, agreed.
>Show me something that does better and I'll switch tomorrow.

There's no limit to perfection, but if you merely don't write C of opportunistic kind, logical errors quickly start to outweigh other types of errors.

Are you willing to die on this hill? I remember an HN post a while ago where both Microsoft and Google said something like 65% of the bugs in C code were related to memory (un)safety.
I don't deny that.
> Show me something that does better and I'll switch tomorrow

Frankly this is a very bad decision. Because now you have C code, Rust code, and yet another language's code, and you're left with a mess that you have to integrate too.

Obviously I was demonstrating that I don't shill for Rust in particular -- I simply believe C/C++ are not cutting it anymore. Rust is not perfect but it solves a sizeable chunk of their problems, that's all.
Most programmers don't have any opinions, so if they use an unopinionated language, they end up using patterns that opinionated people use. So it's better to have one source of truth for opinions, so that we don't end up using the "wrong" opinions of people who talk more than they think.
Compiler/language people are also just "opinionated people" though. Some have good opinions, some have bad opinions. In the worst case you have opinions which are the result of a 'design by committee/community' process.
Single source of truth of opinions can be challenged and changed if one's arguments are good enough. The benefit is that it's easier to see the advantages and disadvantages for those opinions, because they're all in the same place.

This is in contrast to C++, where one organization creates a set of guidelines, another organization creates another set of guidelines. Valid arguments of critique in one organization are not seen in the other.

Single source of truth is also beneficial to compiler authors as well, because they get more feedback how the language is used and why.

Unfortunately you have to pick 2 out of:

- Lack of restrictions

- Safety

- Performance

If you choose safety and no restriction, you pay the price in performance (for GC etc.)

There's a massive Terra Incognita to explore between Rust on one side and Python on the other side (just to pick two extremes).

It's not "2 out of 3", it's a triangle where a language can pick a sweet spot anywhere within the triangle (and ideally, it's not a "sweet spot" either, but more like a "sweet area" where the programmer can pick an actual spot within that area defined by the language).

That sort of extreme flexibility simply does not exist. I mean it does but then you are firmly in the dynamic languages territory and you are forgoing any hope for close-to-the-metal performance.
IMHO it does and its not restricted to dynamic languages (like JS or Python), look at this Zig function signature for instance (just an example from my current dabbling):

   fn setData(comptime pins: anytype, bus: anytype, data: u8) @TypeOf(bus)
In practice this looks and feels like dynamic typing, yet when looking at the compiler output it still resolves to optimal code (since it's "compile-time dynamic typing" not "run-time dynamic typing", but the difference in practice is surprisingly small).
Well you can make OCaml and Rust look like dynamic typing as well by omitting type signatures and squeezing the type inference engine as much as you can but I was under the impression that you have more asked about something that is very relaxed in terms of upfront requirements and be able to tighten it up later?

That's why I claimed that no such language exists.

I was thinking more in terms of "language surface" but it didn't come across, C and Zig have a fairly small language surface (Zig a bit bigger than C), simple language primitives that can be combined quite freely and without much restrictions, but at the same time not carrying a lot of semantics the compiler could use to ensure safety (ignoring the "sloppiness" design warts of C though, like implicit type conversions, allowing accidential uninitialized data, or inverted defaults (mutable vs immutable) - these things are obvious problems but cannot be easily fixed in C or C++ because of backward compatibility requirements - they have been fixed in Zig though).

Rust is quite the opposite, more primitives that carry semantics (most of those in the stdlib though), a stronger but also more rigid type system, but those are pretty much needed for the compiler to guarantee safety.

The million dollar question is of course, can there be a more relaxed Rust with the same safety and performance guarantees, maybe by "squeezing the type inference engine" even more (and letting Rust look or somehow extract information across crate boundaries)? And do Rust programmers even see this as desirable, or are they mostly comfortable in their current sweet spot of the triangle?

I'm not super familiar with Zig, but that appears to be the same as the rust function signature fn setData<P, B>(pins: P, bus: B, data: u8) -> B;

in rust, the P and B are resolved at compile time, not at runtime. If you wanted dynamic dispatch the types would be Box<dyn B>

Yes, it's essentially the same, the comptime attribute on the pins arg is quite important though (in the function body that's not shown).
Again, restrictions that are forced you for a price of safety are one thing. But what I'm complaining about are restrictions that don't have to be there to get borrowchecker working, but rather are there because designers arbitrary decided "it's better this way".
As someone just starting to finally learn Rust, I'm curious what some examples of this might be.
All AFAIK, since I only dabble occasionally in Rust:

The borrow checker works on "struct granularity", but it would be much more flexible and convenient if borrowing would work on memory location granularity (for instance passing a struct reference into a function "taints" the entire struct as borrowed, even if that function only accesses a single item in the borrowed struct - this 'coarse borrowing' restriction then may lead to all sorts of workarounds to appease the compiler, from 'restructuring' your structs into smaller pieces (which then however may fit one borrowing situation, but not another), or using 'semantic crutches' like Rc, Cell or Box.

There are also related restrictions about function call barriers. AFAIK the Rust compiler cannot "peek into" called function bodies to figure out what's actually going on inside those functions (and that information would be very valuable for fine-grained borrow checking), it can only work with the information in the function signature.

Again, disclaimer: take this with a grain of salt since I'm not a daily Rust user, but this is how I understood why Rust feels so restrictive.

> The borrow checker works on "struct granularity", but it would be much more flexible and convenient if borrowing would work on memory location granularity (for instance passing a struct reference into a function "taints" the entire struct as borrowed, even if that function only accesses a single item in the borrowed struct - this 'coarse borrowing' restriction then may lead to all sorts of workarounds to appease the compiler, from 'restructuring' your structs into smaller pieces (which then however may fit one borrowing situation, but not another), or using 'semantic crutches' like Rc, Cell or Box.

That's valid, thanks for pointing it out. I seem to recall the team lately mentioning they are starting to consider fixing that. And yes that's a real productivity killer, happened to me as well in the past.

They made a lot of progress with borrow checker granularity between 1.0 and now. It's much more granular now than before.
> There are also related restrictions about function call barriers. AFAIK the Rust compiler cannot "peek into" called function bodies to figure out what's actually going on inside those functions (and that information would be very valuable for fine-grained borrow checking), it can only work with the information in the function signature.

I don't think it's so much "can not" as it is "will not". Allowing the function signature to be determined by the body can lead to accidentally breaking callers by changing the body.

That, at least, is consistent with other parts of the signature: the input/output types and how the lifetimes of input/output references are related.

So a few things .. Rust is a very good safe language. It also has an unsafe keyword to make the compiler ignore borrows etc.

If you want fast iteration, use python and then hand transpile your code into rust.

Not every tool has to be used at once ... but Python for the idea and rust for the implementation can be the best of both worlds ... :)

Except Rust's unsafe is even worse then any other 'unsafe' languages.

And while you can technically prototype in a other language, speed of iteration is always a bottleneck.

The only way to escape it is if You are an about master of language, the project you are working on and even the feature you are adding.

But that's a verry rare case scenario

I don't disagree btw. As a contractor I have found myself very often in a situation where Rust's slower iteration simply didn't work for me so I got back to Elixir and also relearned and started getting proficient in Golang.

Rust's slower iteration only disappears when you become a pro as you said and that's my main problem with it. I simply can't invest as much time and effort for free.

Rust is just a different language. It's not C-like or Java-like plus checks or anything like that.

It's a value oriented language. Variables mean something completely different than in any other common language. Everything is about the values and where they are kept at the moment.