Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I am wondering about this. In C the nice thing is that you usually change things locally. A line of code does not depend on a million other things. I can't quite see how this works in Rust... At least in C++, template, overloading, etc. can make a single line depend on a lot of things.


> I can't quite see how this works in Rust...

You won't understand it unless refactor some Rust programs.

Bunny summed it up rather well. He said in most languages, when pull on some thread, you end disappearing into a knot and you're changes are just creating a bigger knot. In Rust, when pull on a thread, the language tells you where it leads. Creating a bigger knot generally leads to compile errors.

Actually he didn't say that, but I can't find the quote. I hope it was something like that. Nonetheless he was 100% spot on. That complexity you bemoan about the language is certainly there - but it's different to what you have experienced before.

In most languages, complex features tend lead to complex code. That's what made me give up on Python in the end. When you start learning Python, it seems a delightfully simple yet powerful language. But then you discover metaclasses, monkey patching, and decorators, which all seem like powerful and useful tools, and you use them to do cool things. I twisted Python's syntax into grammar productions, so you could write normal looking python code that got turned into an LR(1) parser for example. Then you discover other peoples code that uses those features to produce some other cute syntax, and it has a bug, and when you look closely your brain explodes.

As you say C doesn't have that problem, because it's such a simple language. C++ does have that problem, because it's a very complex language. I'm guessing you are making deduction from those two examples that complex languages lead to hard to understand code. But Rust is the counter example. Rust's complexity is forces you to write simple code. Turns out it's the complexity of the code that matters, not the complexity of the language.


I only have limited experience with Rust (only playing around a bit). But it seems the language forces you to structure the code in a specific way (and you seem to agree). This is good, because it prevents you from making a mess (at some level at least). But what others report (and it matches my limited experience) is that it makes the structure of the code very rigid. So I do not quite see how this does not limit refactoring? What some of you describe in this thread is type-directed refactoring (i.e. you change a type and the compiler tells what you need to change), but is this not limited to relatively basic changes?

In C, you can make partial changes and accept a temporary inconsistency. This gives you a lot of flexibility that I find helpful.


I'll put it like this. Rust causes you to refactor more often and do a full refactoring at once (because the interfaces are so rigid), but it makes it easy to do a full (and correct) refactoring very quickly. Think of it as docs being part of the interface and the compiler forcing you to always update the docs. That's an amazing property


> So I do not quite see how this does not limit refactoring?

Yes it limits refactoring. But all languages provide syntactic and semantic constraints you must operate within. You can't just add line noise to a C program and expect it to compile.

So your complaint isn't that there are limits, it's that there is less of them in C, so it's easier to make changes in C without putting too much thought into it. That is absolutely correct. It's also true that's is far easier to introduce a bug in C code when you refactor than it is in Rust, and that's so because it's harder to write buggy code in Rust that gets past the compiler.

Perhaps an example. Consider:

    inline char foo(uint8_t i, char a[]) { return *(a + 1); }
What does this code do? In C or C++, it's impossible to answer. If i overflows, it's UB. If you go past the end of the array, it's UB. Since it's inlined, there is no way to know if either could happen. If compiler can prove some particular instance is UB, it can do whatever it damned well pleases, without informing the poor programmer. There are lots of worse examples, particularly in C++.

In Rust, it's entirely predictable what happens for any given input, as Rust forbids UB in safe code. But in order to pull that off, Rust forces you to re-write the above function, perhaps into something like this:

    fn foo(i: u8, a: &[u8]) -> u8 { a[(i + 1) as usize] }
or:

    fn foo(i: u8, a: &str) -> char { a.chars().skip((i as usize) + 1).next().unwrap() }
or many other variations depending what you actually need to do. Notice for example you where forced to say whether you're happy with incrementing 255 to 256. If you weren't, you would write it as (i + 1) as usize.

Yes, it's more mental effort. In return, you don't get your arse handed to you on a platter because you recompiled with a newer, smarter version of the compiler that noticed you violated some language rule only a language lawyer would know, and took advantage of it.


> A line of code does not depend on a million other things.

But it does! To qoute my top-level comment:

> What about about race conditions, null pointers indirectly propagated into functions that don't expect null, aliased pointers indirectly propagated into `restrict` functions, and the other non-local UB causes?

In other words: you set some pointer to NULL, this is OK in that part of your program, but then the value travels across layers, you've skipped a NULL check somewhere in one of those layers, NULL crosses that boundary and causes UB in a function that doesn't expect NULL. And then that UB itself also manifests in weird non-local effects!

Rust fixes this by making nullability (and many other things, such as thread-safety) an explicit type property that's visible and force-checked on every layer.

Although, I agree that things like macros and trait resolution ("overloading") can be sometimes hard to reason about. But this is offset by the fact that they are still deterministic and knowable (albeit complex)


This is true to some degree, but not really that much in practice. When refactoring you just add assertions for NULL (and the effect of derefencing a NULL in practice is a trap - that it UB in the spec is completely irrelevant. in fact it helps because it allows compilers to turn it a trap without requiring it on weak platforms). Restrict is certainly dangerous, but also rarely used and a clear warning sign, compare it to "unsafe". The practical issues in C are bounds checking and use-after-free. Bounds checking I usually refactor into safe code quickly. Use-after-free are the one area where Rust has a clear advantage.


I agree that in practice NULL checking is one of the easier problems. I used it because it's the most obvious and easy to understand. I can't claim which kinds of unsoundness in C code are more common and problematic in practice. But that's not even interesting, given that safe Rust (and other safe languages) solve every kind of unsoundness.

> in fact it helps because it allows compilers to turn it a trap without requiring it on weak platforms

The "shared xor mutable" rule in Rust also helps the compiler a lot. It basically allows it to automatically insert `restrict` everywhere. The resulting IR is easier to auto-vectorize and you don't need to micro-optimize so often (although, sometimes you do when it comes to eliminating bound checks or stack copies)

> Restrict is certainly dangerous, but also rarely used and a clear warning sign, compare it to "unsafe".

It's NOT a clear warning sign, compared to `unsafe`. To call an unsafe function, the caller needs to explicitly enter an `unsafe` block. But calling a `restrict` function looks just like any normal function call. It's easy to miss an a code review or when upgrading the library that provides the function. That the problem with C and C++, really. The `unsafe` distinction is too useful to omit.


You are not wrong, but also not really right. In practice, I never had a problem with "restrict". And this is my point about Rust being overengineered. While it solves real problems, you need to exaggerate the practical problems in C to justify its complexity.


> In practice, I never had a problem with "restrict".

That's exactly because it's too dangerous and the developers quickly learn to avoid it instead of using it where appropriate! Same with multithreading. C leaves a lot of optimization on the table by making the available tools too dangerous and forcing people to avoid them altogether.

That's how you get stuff like memory-safe Rust PNG decoders being 1.5x faster than established C alternatives that had much more effort put into them (https://www.reddit.com/r/rust/comments/1ha7uyi/memorysafe_pn...). Or the first parallel CSS engine being written in Rust after numerous failed attempts in C++ (https://www.reddit.com/r/rust/comments/7dczj9/can_stylo_be_i...). Read those threads in full, there are some good explanations there.

> you need to exaggerate the practical problems in C

I thought, the famous "70% of vulnerabilities" report settled this once and for all.


Na, sorry. The "70%" is just nonsense. And cherry picking individual benchmarks too.


> The "70%" is just nonsense.

Care to elaborate why?

> And cherry picking individual benchmarks too.

Do you have any general, comprehensive benchmarks or statistics that would indicate the opposite? I would include one if I had one at hand, because that would be a stronger argument! But I'm not aware of such benchmarks. I have to cherry pick individual projects. I don't want to.

I still claim that, as a general trend, Rust replacements are faster while also being less bug-prone and taking much less time to write. Another such example is ripgrep.


You're focusing purely on the problem-fixing aspects, but Rust's expressiveness is why I like it.

C can't match that. In C, you're basically acting as a human compiler, writing lots of code that could be generated if you used a more expressive language. Plus, as has been mentioned, it supports refactoring easily and safely better than any language outside of the Haskell/ML space.

The advantages of Rust are a package which includes safety, expressiveness, refactoring support. You don't need to exaggerate anything for that package to make sense.


I am not criticizing anyone for preferring Rust. I reject the idea that its complexity needs to imposed on everybody because of "safety"


The "impose on everybody" is based on clear, objective metrics at the national and international level.

The fact that you, as an individual, prefer to use an unsafe, weakly typed language isn't very relevant to that.

The issue is not that it's not possible to write secure programs in C, the issue is that in practice, on average, people don't.

Pushing the use of memory-safe languages will reduce the number of security vulnerabilities in the entire software ecosystem.


> This is true to some degree, but not really that much in practice. When refactoring you just add assertions for NULL (and the effect of derefencing a NULL in practice is a trap - that it UB in the spec is completely irrelevant.

This happens a lot in discussion about programming complexity. What you are doing is changing the original problem to a much simpler one.

Consider a parsing function parse(string) -> Option<Object>

This is the original problem, "Write a parsing function that may or may not return Object"

What a lot of people do is they sidetrack this problem and solve a much "simpler problem". They instead write parse(string) -> Object

Which "appears" to be simpler but when you probe further, they handwave the "Option" part to just, "well it just crashes and die".

This is the same problem with exceptions, a function "appears" to be simple: parse(string) -> Object but you don't see the myriads of exceptions that will get thrown by the function.


I guess it depends on what the problem is. If the problem is being productive and being able to program without pain "changing the original problem to a much simpler one" is a very good thing. And "crash and die" can be a completely acceptable way to deal with it. Rust's "let it panic" is not at all different in this respect.

But in the end, you can write Option just fine it C. I agree though that C sometimes can not express things perfectly in the type system. But I do not agree that this is crucial for solving these problems. And then, also Rust can not express everything in the type system. (And finally, there are things C can express but Rust can't).


> in the end, you can write Option just fine it C.

No, you can't. In the sense that the compiler doesn't have exhaustiveness checks and can't stop you from accessing the wrong variant of a union. An Option in C would be the same as manually written documentation that doesn't guarantee anything.

std::optional in C++ is the same too. Used operator* or operator-> on a null value? Too bad, instant UB for you. It's laughably bad, given that C++ has tools to express tagged unions in a more reliable way.

> And then, also Rust can not express everything in the type system. (And finally, there are things C can express but Rust can't).

That's true, but nobody claims otherwise. It's just that, in practice, checked tagged unions are a single simple feature that allows you to express most things that you care about. There's no excuse for not having those in a modern language.

And part of the problem is that tagged unions are very hard to retrofit into legacy languages that have null, exceptions, uninitialized memory, and so on. And wouldn't provide the full benefit even if they could be retrofitted without changing these things.


Yes, all I am saying is that we have to be honest what level of problems we are solving when we encounter a complicated solution.

The solution have to scale linearly with the problem at hand, that is what it means to have a good solution.

I agree with the article that Rust is overkill for most use cases. For most projects, just use a GC and be done with it.

> But I do not agree that this is crucial for solving these problems. And then, also Rust can not express everything in the type system.

This can be taken as a feature. For example, is there a good reason this is representable?

struct S s = 10;

I LOVE the fact that Rust does not let me get away with half-ass things. Of course, this is just a preference. Half of my coding time is writing one-off Python scripts for analysis and data processing, I would not want to write those in Rust.

> But in the end, you can write Option just fine it C.

Even this question have a deeper question underneath it. What do you mean by "just fine"? Because to me, tagged enums or NULL is NOT the same thing as algebraic data types.

This is like saying floating points are just fine for me for integer calculations. Maybe, maybe for you its fine to use floating points to calculate pointers, but for others it is not.


>When refactoring you just add assertions for NULL

This is a line of thinking I used to see commonly when dynamic typing was all the rage. I think the difference comes from people who view primarily work on projects where they are the sole engineer vs ones where they work n+1 other engineers.

"just add assertions" only works if you can also sit on the shoulder of everyone else who is touching the code, otherwise all it takes is for someone to come back from vacation, missing the refactor, to push some code that causes a NULL pointer dereference in an esoteric branch in a month. I'd rather the compiler just catch it.

Furthermore, expressive type systems are about communcation. The contracts between functions. Your CPU doesn't case about types - types are for humans. IMO you have simply moved the complexity from the language into my brain.


This was about refactoring a code base where a type is assumed to be non-null but it is not obvious. You can express also in C on interfaces that a pointer is non-null.


> interfaces

You mean obsolete and subtly wrong comments buried among preprocessor directives in some far-upstream header files?


Preprocessor enters the scene.

Then we have the functions that might be re-entrant or not, in the presence of signals, threads,...




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

Search: