Brian Lovin
/
Hacker News
Daily Digest email

Get the top HN stories in your inbox every day.

blub

This reads like praising by faint criticism.

The number one problem Rust has now is that it’s overall not significantly better than C++ and sometimes the reverse is true. I’ve learned for example quite a good chunk of Rust, implemented a project (actually a rewrite from C++) and now I’ve mostly reverted to C++ and Go. The only reason that I’d use Rust is if I had to ensure that a program is memory safe (e.g. it’s planned to be handed over to developers unexperienced with C++, it will be heavily changed in the near future, etc).

It’s no coincidence that I’ve only learned a chunk of Rust - I skipped async, concurrency and parts of the standard library which I didn’t need. The language and the ecosystem are big and complex, very comparable to the complexity of C++. It’s in a different difficulty league compared to C or Go and I wouldn’t say that it’s hard per se to learn, as the concepts themselves are similar to C++, but there’s a lot to study, remember and categorize and map in relation to how things are done in e.g. C++. One exception is Rust’s equivalent to C++ template-heavy code which is simply not worth wasting one’s life on. Whoever writes that is either a library developer or an asshat.

Compile times are too long and what makes it worse is that everything seems to be distributed as source and has to be compiled. Because cargo is so convenient to use, dependencies are completely out of control: adding a single crate can end up pulling in dozens of dependencies. In my case I ended up with ~150 for ~5 crates!

All in all the benefits barely outweigh the costs for me, especially since I know C++ well. Rust to me is a language for large-scale or low-level projects that need solid performance and provable memory safety, but it’s overkill for many things where one can use Go or a simplified dialect of C++.

epage

> The number one problem Rust has now is that it’s overall not significantly better than C++ and sometimes the reverse is true.

Maybe we are coming at this from different angles but I disagree. I remember seeing some discussions in the C++ community about challenges with using `std::string_view` that seemed like they were discouraging it and thinking to myself "this isn't a problem in Rust".

In fact, I refactored a crate I maintain from cloning objects all over the place to using references. I looked at what i did with my C++ hat on and realized that I'd consider myself irresponsible to do the same in C++ without a *very* high bar to justify it, not just because of the cost of the global analysis to ensure it was safe but the fact that that global analysis would need to be repeated on every subsequent change. Being in Rust, it compiled and I knew I was good. I even had an idea for reducing even more allocations that I thought was safe (20+ years of C++ experience, 10 in life-or-death mission-critical software) and the compiler found a case where I was wrong and the approach doesn't work.

blub

The keyword is overall. One wouldn’t typically pick a programming language purely based on how well it handles references.

Generally Rust does memory safety better than C++, that’s painfully clear as soon as one uses the language and compares it with what happens in a C++ project with average developers. But memory safety is just one dimension of evaluation.

nicoburns

> The language and the ecosystem are big and complex, very comparable to the complexity of C++. It’s in a different difficulty league compared to C...

I think this is only true if you're already familiar with C-family languages (C and C++). As someone who came to lower-level programming with a JavaScript/Python/PHP background, I found Rust significantly easier to learn than either C or C++ (C was easy to do hello world, but jumped to WTF-level complexity as soon as I needed to interact with a 3-party library).

Let me put it this way: would you trust a developer who was new to C or C++ to write secure code that's free of memory safety issues, undefined behaviour and multi-threading race conditions? You can do that with Rust, and IMO that's a huge win.

oconnor663

> I think this is only true if you're already familiar with C-family languages (C and C++).

This comes up a lot. A lot of people learn C or C++ in school, so the distinction between learning those languages and learning programming in general gets blurry. Being full of UB that "works on my box" also makes things blurry: If you don't know how to write sound programs, have you actually "learned" the language?

I think learning C or C++ takes 2-5 years. Many people forget that, because it was mixed in with learning other things. Many others quit the learning process partway through without realizing it. Rust forces you to actually learn Rust to write Rust, which can make it seem like there's more to learn by comparison. But if you restrict yourself to the safe subset, I think there's actually much less to learn.

blub

C++ and Rust are very deep multi-paradigm languages, with many features that try to cover a lot of domains - that’s why they are complicated. Sure, C++ has the decades of backwards compatibility and weaker memory safety, but in the end it’s clear they belong to the same difficulty class, even if C++ is likely harder to learn.

pjmlp

Provided they never use unsafe blocks and the third party dependencies have been inspected for code safety invariants when called from safe code with bad parameters.

the__alchemist

I'd argue that even without the memory safety and outstanding tooling, Rust (at least no-allocator, embedded Rust) maps 1:1 with C and C++ with nicer syntax.

Some examples:

  - No DRY between headers and source files
  - Arrays are explicitly described as such in type signatures, ie not as pointers to the type they contain
  - No pre-processor
  - Cleaner, more explicit pattern matching syntax
  - No prefixing all constants/structs/enums with the module name
  - A struct or enum is declared once, with its name; ie no _t, _e, _s suffix and name duplication
  - Type inference
  - Explicit handling of enum variants

greggman3

Why is a preprocessor bad? I do tons of platform level programming. Let’s say I’m doing Metal. There is #if ISO, #if APPLETV, #if MACOS stuff around the code. Sure, I could over engineer by trying to pass in some specialization functions but it can get extremely tedious and ugly to deal with. There’s a time and place for both.

If I recall correctly, C# didn’t ship with one but it was added. For one it makes it easier to use the standard build system (just compile all the files referenced) rather than have to use an outside build system that knows when to include linux.xxx and when to incluide windows.xxx etc.

Of course I’m aware of some of the drawbacks but I’d prefer with than without.

Rusky

Rust has equivalent functionality but it works on syntax trees rather than token streams, so you write `#[cfg(target_os = "macos")]` as an attribute rather than wrapping an arbitrary chunk of text in `#if`/`#endif`.

pjmlp

- use modules

- use std::array and std::vector

- Most stuff can be use with templates and constexpr

- use namespaces

- just like in C++

- auto type inference

- enum classes

It is about time to replace C with C++.

the__alchemist

Didn't know that! I've been translating several C and C++ (mostly C) codebases (embedded), and haven't seen those. Probably because C, and older C++ specs / design patterns.

pcwalton

Pattern matching is much more powerful in Rust than in C++, even with Boost libraries. Type inference is likewise more powerful, as you can overload on return type.

AnimalMuppet

How do modules solve the first one?

royjacobs

"The number one problem Rust has now is that it’s overall not significantly better than C++"

This might be true for your specific use cases, but I've worked on reasonably large projects in both Rust and C++ and the fact that I don't have to chase various issues related to use-after-free, returning local variables, dealing with external libraries or general metaprogramming is definitely causing me to mark Rust as significantly better.

As always, if you're a C++ expert you'll bound to disagree, YMMV etc.

joshmarinacci

> It’s no coincidence that I’ve only learned a chunk of Rust - I skipped async, concurrency and parts of the standard library which I didn’t need.

I think this is why you don't think it's significantly better. I use JS and Python for most of my work, but when I want something that must do threading correctly or is performance bound, then I go to Rust over C++. That level of safety is the critical value for me.

(I also find it significantly easier to compile Rust than the typical C++ build chain, especially when doing cross platform work, but that's more a compiler than language issue.)

TillE

I went through pretty much the exact same process several years ago, coming from the same place of deep C++ expertise. Rust has some cool stuff, but it also requires learning a ton of new practices, and C++ is getting better fast enough that it just doesn't seem worthwhile to switch.

zozbot234

Are you saying that C++ doesn't have you learn a "ton of new practices" over time? That only makes sense if you're still writing C++98.

pcwalton

> Because cargo is so convenient to use, dependencies are completely out of control: adding a single crate can end up pulling in dozens of dependencies. In my case I ended up with ~150 for ~5 crates!

Being able to share code easily is a good thing, and it's one of the biggest draws of Rust compared to C++. You will see the same thing once C++ modules and package managers take off; in fact, if you don't, that means that C++ modules have failed.

zozbot234

> The number one problem Rust has now is that it’s overall not significantly better than C++

Username is definitely relevant! http://www.paulgraham.com/avg.html

> and what makes it worse is that everything seems to be distributed as source and has to be compiled.

You can distribute a binary shared library written in Rust, but make sure you're using a C-style public interface to preserve ABI stability and make it accessible to code written other languages. It's generally feasible to design such an interface and build Rust-friendly wrapper code that can be included in client projects - there are crates that will specifically make this easier. C++ punts on the whole ABI issue and is now facing ecosystem-wide breakage as a result.

epage

> Every file in the tests directory is a separate crate, compiled to its own executable. This is a reasonable decision, with undesirable consequences:

Other problems with this:

- You have to link each test file which can be slow

- You get less parallel test running because you've broken things up into smaller parallel batches. cargo-nextest is a proof-of-concept that shows this problem [0]

- Fixture initialization reuse becomes trickier

Projects like cargo explicitly combine all integration tests into a single binary. I'd love it if we explored implicitly rolling everything up into a single test binary except for any explicit test targets defined by the user.

[0] https://nexte.st/book/benchmarks.html

scns

> You have to link each test file which can be slow

Using Mold [0] as the linker might help. Written by the author of lld.

  Program (linker output size):  GNU gold | LLVM lld |  mold

  Chrome 96 (1.89 GiB):            53.86s |   11.74s | 2.21s

  Clang 13 (3.18 GiB):             64.12s |    5.82s | 2.90s

  Firefox 89 libxul (1.64 GiB):    32.95s |    6.80s | 1.42s
[0] https://github.com/rui314/mold

edit: add citation, info, times, link, will -> might, and formatting.

tlamponi

FWIW, I tried it on a rust project here, the difference was in the noise range only; that way I knew that I was rather IO limited (linking produces lots of IO) and also that mold benchmarks _may_ potentially overpromise a bit much, at least when applied at my use case.

scns

On a NVMe or SATA SSD?

Aissen

Very interesting how advanced those grievances are. If that's not a sign of maturity, I don't know what is.

Otherwise, seems like a good list, thanks for sharing @woodruffw.

ncmncm

In general, if there is nothing you hate about a language, it always means you don't know it very well.

What do you hate about X is a useful interview question, with discussion of the effect, and alternative formulations that might have saved it.

bluefirebrand

That's a great thought, I'm going to keep that one tucked away as a question in my back pocket.

assbuttbuttass

> it’s sometimes nice to have the assurance that no code being executed can possibly panic.

I struggling to think of any language that can provide this guarantee. Even Python can segfault while executing C extensions. Maybe java?

oefrha

You can easily segfault in pure Python:

  import sys
  sys.setrecursionlimit(1048576)
  def recurse(): recurse()
  recurse()

orf

A slightly simpler way is just:

    import ctypes
    ctypes.string_at(0)

thesuperbigfrog

Ada has a set of High Integrity Restrictions that define pragmas that turn off exceptions:

"No_Exceptions - Raise_statements and exception_handlers are not allowed. No language-defined run-time checks are generated; however, a run-time check performed automatically by the hardware is permitted."

Source: http://www.ada-auth.org/standards/12rm/html/RM-H-4.html

If the underlying operating system or board support system have similar support then panic situations would be avoided entirely.

While this is a highly-specialized case, it is defined and supported by high integrity Ada implementations / runtimes.

bb88

You don't want to do that. It's an optimization, not a "The compiler will guarantee there won't be exceptions thrown."

https://www.adaic.org/resources/add_content/docs/95style/htm...

"If you disable exception checks and program execution results in a condition in which an exception would otherwise occur, the program execution is erroneous. The results are unpredictable"

ajkjk

But most languages don't have all their libraries willing to intentionally panic, so it's not a big deal in practice.

ansible

It would be nice to have an overall scorecard for a library. It would show a bunch of stuff related to the crate: existence of unit tests, fuzz testing, and measures of the test coverage, if the code passes clippy checks, which edition of Rust, and such. We could also grade crates by how many open tickets they have which haven't seen a response, etc. This wouldn't be intended to shame the library maintainers, but more to indicate which projects are active, and which ones have issues fixed promptly.

It would be easy enough to also scan the source for sources of panics.

Retr0id

Vanilla python can also segfault, in certain edge cases.

    eval((lambda:0).__code__.replace(co_consts=()))

amilios

This code throws a missing attribute error on Python 3.7? 'code' object has no attribute 'replace'. Is it for 2.7?

Retr0id

3.8+, iirc

kazinator

Sometimes to have the assurance means in some specific code, not always: the code is entered via some entry point, and guaranteed to reach one of its exit points without hitting a panic.

loeg

I think it's a property that you could validate via static analysis in relatively strongly-typed languages. I agree it would be impossible for Python, just because everything can change at runtime.

Deukhoofd

To do so in C# requires messing with C libraries, but generally most behaviour that could actually segfault throws an exception instead.

xanathar

Popping in my mind:

- System.Diagnostics.Debug.Assert (on debug builds)

- Envinroment.FailFast (this is precisely what panic! is usually used for)

- void BlowUp() { BlowUp(); }

But, in reality, any unexpected Exception: how would you properly handle an unexpected NRE or index out of bounds? Either swallowing the exception (bad) or aborting.

garaetjjte

panic is just different name for exception though.

throwawayninja

Exceptions can be handled, so in rust an "exception" is a "Result<T,U>". A panic means the program terminates, guaranteed. This allows us to ignore hugely difficult edge cases when interacting with hardware / OSes and still ensure the code meets safety guarantees; if the code isn't executing it cannot invalidate any invariants.

adastra22

Panic is what Rust calls exceptions.

dragonwriter

> Panic is what Rust calls exceptions.

Not exactly; “panic” is what Go calls exceptions.

Rust “panic!” macro, though, is more of a gentle abort (I guess it's like an uncatchable exception, but that qualifier so radically changes the nature that I can't see it as really the same kind of thing.)

skywal_l

"Crystal has a wonderful and extremely ergonomic macro system that Rust could learn from, one that doesn’t require ad-hoc reinterpretation of language tokens and that integrates seamlessly with syntax highlighting in editors".

What about zig, where there is no macros per-se, and you just write zig code which is then interpreted by the compiler...

woodruffw

Author here: I haven't written any Zig, but I've read a little bit about `comptime` (which I thought was very cool).

I like compile-time programming, but I also recognize that it's programmer catnip: we love it because it lets us be clever, including in ways that aren't conducive to maintainability. That's a big part of why I like Crystal's macro system: I think it does a good job of balancing readability, expressivity, but also constraining the programmer away from compile-time gymnastics.

But like I said, I have no Zig experience, so it's very possible that Zig has much better ergonomics than I'm afraid of!

nickysielicki

I’d like to add one: There is too much control at the language level between static and dynamic dispatch. Slimming down your binaries by using dynamic dispatch requires significant code changes when it shouldn’t necessarily have to.

I don’t know what I’d propose instead that wouldn’t be just as bad, but I think it’s a wart.

pornel

Rust initially didn't require `dyn` prefix for dynamic dispatch (trait object types), but the lack of syntactic distinction was confusing to users. Rust is low-level enough that there is a semantic difference which one you use (Rust calls it "object safety"), so you can't just pretend there's none.

Rust is also very focused on performance, and Rust users do care about making such trade-offs consciously. You care about binary size, I may care about inlined autovectorized code.

nickysielicki

> Rust is also very focused on performance, and Rust users do care about making such trade-offs consciously. You care about binary size, I may care about inlined autovectorized code.

Everyone can agree with this. The question is whether this ought to be a property of the target platform or whether this ought to be a property of the software itself. Rust implicitly takes the stance that it is a property of the software.

Suppose we had a chip today that runs poorly due to icache thrash, so you rewrite portions of it with dynamic dispatch. Then someday we get a chip with an insanely large icache that would benefit from static dispatch. You rerewrite it. Or today if you work on embedded linux and want to use a rust library whose primary users are on powerful servers.

I will concede that I have no idea whether it’s possible to come up with a reasonable abstraction for this, because you certainly know more than me.

pornel

There are already plenty of architectural differences that change optimal trade-offs in the code, e.g. memory speed affects lookup tables vs recomputation choice and inlined data vs pointer chasing, branch prediction cost affects conditionals vs branchless/redundant code, data layout needs to be adjusted for SIMD and cache line sizes, memory hierarchy and core topology limit when it's beneficial to parallelize algorithms, and so on. These trade-offs don't even require oddball futuristic CPUs, and already vary significantly even within existing CPU families.

There are some languages that focus on "what" and magically optimize "how" (ISPC, Halide, and good'ol SQL), but for better or worse, Rust has chosen the "portable assembly" angle and micromanagement of everything. The upside is that you have very few surprises and "performance cliffs", and rest assured that if something is slow, you have enough control to fix it (as opposed to e.g. JIT-based language).

pjmlp

I am perfectly fine not having such difference in C++.

pcwalton

I'm not, because of the object slicing problem. Value types and inheritance don't mix well, and the way Rust expresses inheritance with explicit "dyn" traits solves the problem neatly.

undefined

[deleted]

uluyol

Can these code changes be automated with a tool?

One of the strengths of Go and Java is the ease of writing tools. C++ on the other hand is a mess since it's hard to look through templates (concepts probably help) and macros (good luck).

pjmlp

Do just like in Go and Java, use an IDE.

loeg

Not sure I agree with this. The relative similarity between static and dynamic dispatch in C++ is pretty confusing (to me) (edit: as in, 'virtual' vs non-'virtual' methods).

chubot

?? They appear completely different in C++, i.e. virtual functions vs. templates

https://eli.thegreenplace.net/2013/12/05/the-cost-of-dynamic...

I also wish they were more similar in both C++ and Rust. I find that most programs get more dynamic as they get bigger, get more usage, and get more extensible ... but the tendency is to want them to be as efficient as possible at the beginning.

loeg

> They appear completely different in C++, i.e. virtual functions vs. templates

Virtual vs non-virtual functions.

chlorion

Would a regular non-virtual non-template method be considered single dispatch? I am pretty sure this is correct but I've never actually thought about this until I read your comment!

nickysielicki

Oh, let’s not set the bar so low by comparing to C++. ;)

Two ends of the spectrum and I think the ideal is somewhere in the middle.

rami3l

Instead of enums, how about something similar to Scala's sealed trait syntax to tell the compiler that I'm actually doing a static dispatch?

zozbot234

Enums and "sealed" classes enable closed vs. open dispatching. Static (compile-time) vs. dynamic would effectively be devirtualization.

pitaj

On the IntoIterator topic: I do think it was a mistake to `impl IntoIterator for &[mut] T` for the collections. I think it would have been better to just standardize on `.iter[_mut]()` instead. Would have avoided the whole "IntoIterator for arrays" issue and would be more explicit.

pavon

I'm just relearning Rust after first using it in the pre-1.0 days. Predicting what kind of iterator I'm going to get in what circumstances has definitely been a stumbling block for me. Sometimes it makes sense in retrospect (String::chars() must be producing copies not references, because it doesn't hold data as char internally, duh), but it is an area of the language where I frequently get unexpected compiler compiler errors.

epage

For me, the reason I like an explicit `.iter()` is because its more refactor-friendly. If I have an existing loop over `&collection` but want to call an iterator method, I have to switch to `collection.iter().enumerate()`.

pie_flavor

Another thing that would have avoided the whole 'IntoIterator for arrays' issue would have been implementing IntoIterator for arrays. What benefit would your version provide over the 2021 edition as-is, and why would it be worth the major consequence of fracturing the iterator system into T, &T, and &mut T, as opposed to &T and &mut T just being special cases of T?

pitaj

The whole IntoIterator for arrays issue was that there was existing code using IntoIterator of the slice deref, which was incompatible with an array IntoIterator impl that would take precedence. A compiler hack was introduced to hide the array implementation on older editions. Requiring `.iter()` to get a reference iterator install of just doing it automatically would have avoided the need for a hack.

pie_flavor

That would require into_iter to behave differently from every other method.

andrewmcwatters

> If that sounds extremely generic to you, it’s because it is! Here are just a few of the ways IntoIterator is used in the wild, using a generic Container<T> for motivation:

> • For producing “normal” borrowing iterators: &T for T in Container<T>

> • For producing iterators over mutable references: &mut T for T in Container<T>

> • For producing “consuming” (i.e., by-value) iterators: T for T in Container<T>

> • For producing “owned” (i.e., copying or cloning) iterators: T for T in Container<T: Clone>1

Life is too short for this kind of unreadable crap.

goombacloud

For panic detection or overflow detection there are static analysis solutions but I hope they get more integrated and standardized (as was attempted in cargo-verify)

sophacles

I'd like to see a `#[no_panic]` tag similar to the `#[no_std]` one.

woodruffw

Yep, exactly something like this!

Implementation is the challenging part: the standard library and community ecosystem is full of legitimate uses of panic, and there are instances where panics might appear in the source code of dependencies but not in (optimized) builds.

nicoburns

> full of legitimate uses of panic

Yes, but the whole point of `no_panic` is that in this context there are no legitimate uses of panic. If that means making a bunch of parallel APIs that return results for this super-correct code then so be it.

gpderetta

surely it should be #[DONT_PANIC] in large friendly letters.

sophacles

Hah, nice one. I think we could get the unicode folks to accept an emoji form of this: https://thebullelephant.com/wp-content/uploads/2016/12/dontp... and alternately accept `#[$UNICODE_FOR_SAID_EMOJI]`.

dureuill

Or implied by the #[keep_calm] attribute

pdimitar

That, or a subset of Rust that has all the panicking functions removed and only has std API that works with Result and Option.

sebzim4500

Presumably using `#[no_panic]` would require `#[no_std]`? Would you be allowed to use unchecked arithmetic, which panics in dev builds but not release builds?

chrismorgan

Why would #[no_panic] require #[no_std]? There are perfectly legitimate reasons for not wanting panicking in code that uses std, and it’s possible to panic without std.

vbezhenar

How to you expect it to work?

adament

Could you not enforce that functions tagged can only call other functions with the tag and must not call panic?

This would not protect against stack overflow or similar environmental panics.

loeg

Yeah, I think I agree with all of this.

bazurbat

The thing I've hated from the day 1 and still can not forget is the reserving of the "type" keyword. Basically every module has these ugly "tp" or "typ" or "type_" structure members everywhere but type aliases are rarely seen in comparison.

zozbot234

Raw identifiers can be used to get around any reserved keyword. So r#type would be as idiomatic a workaround as any. Arguably a lot clearer than the alternatives.

Daily Digest email

Get the top HN stories in your inbox every day.