Show HN: Plexe – ML Models from a Prompt (github.com)
74 points by vaibhavdubey97 6h ago 37 comments
Nnd – a TUI debugger alternative to GDB, LLDB (github.com)
190 points by zX41ZdbW 7h ago 58 comments
Matt Godbolt sold me on Rust by showing me C++
238 LorenDB 174 5/6/2025, 5:51:03 PM collabora.com ↗
Then you top it on with `?` shortcut and the functional interface of Result and suddenly error handling becomes fun and easy to deal with, rather than just "return false" with a "TODO: figure out error handling".
SerenityOS is the first functional OS (as in "boots on actual hardware and has a GUI") I've seen that dares question the 1970s int main() using modern C++ constructs instead, and the API is simply a lot better.
I can imagine someone writing a better standard library for C++ that works a whole lot like Rust's standard library does. Begone with the archaic integer types, make use of the power your language offers!
If we're comparing C++ and Rust, I think the ease of use of enum classes/structs is probably a bigger difference. You can get pretty close, but Rust avoids a lot of boilerplate that makes them quite usable, especially when combined with the match keyword.
I think c++, the language, is ready for the modern world. However, c++, the community, seems to be struck at least 20 years in the past.
A long time ago, there was talk about a similar concept for C++ based on exception objects in a more "standard" way that could feasibly be added to the standard library, the expected<T> class. And... in C++23, std::expected does exist[1], and you don't need to use exception objects or anything awkward like that, it can work with arbitrary error types just like Result. Unfortunately, it's so horrifically late to the party that I'm not sure if C++23 will make it to critical adoption quickly enough for any major C++ library to actually adopt it, unless C++ has another massive resurgence like it did after C++11. That said, if you're writing C++ code and you want a "standard" mechanism like the Result type, it's probably the closest thing there will ever be.
[1]: https://en.cppreference.com/w/cpp/utility/expected
However it seems like C++ wants to only provide this kind of pattern via monadic operations.
This isn't really true since Rust has panics. It would be nice to have out-of-the-box support for a "no panics" subset of Rust, which would also make it easier to properly support linear (no auto-drop) types.
Even then, though, I do see a need to catch panics in some situations: if I'm writing some sort of API or web service, and there's some inconsistency in a particular request (even if it's because of a bug I've written), I probably really would prefer only that request to abort, not for the entire process to be torn down, terminating any other in-flight requests that might be just fine.
But otherwise, you really should just not be catching panics at all.
you could have a panic though, if you wrongly make assumptions
Panics are a runtime memory safe way to encode an invariant, but I will generally prefer a compile time invariant if possible and not too cumbersome.
However, yes I will panic if I'm not already using unsafe and I can clearly prove the invariant I'm working with.
I will fight against program aborts as hard as I possibly can. I don't mind boilerplate to be the price paid and will provide detailed error messages even in such obscure error branches.
Again, speaking only for myself. My philosophy is: the program is no good for me dead.
That may be true, but the program may actually be bad for you if it does something unexpected due to an unforeseen state.
Return an AllocationError. Rust unfortunately picked the wrong default here for the sake of convenience, along with the default of assuming a global allocator. It's now trying to add in explicit allocators and allocation failure handling (A:Alloc type param) at the cost of splitting the ecosystem (all third-party code, including parts of libstd itself like std::io::Read::read_to_end, only work with A=GlobalAlloc).
Zig for example does it right by having explicit allocators from the start, plus good support for having the allocator outside the type (ArrayList vs ArrayListUnmanaged) so that multiple values within a composite type can all use the same allocator.
>Also many functions use addition and what is one supposed to do in case of overflow?
Return an error ( https://doc.rust-lang.org/stable/std/primitive.i64.html#meth... ) or a signal that overflow occurred ( https://doc.rust-lang.org/stable/std/primitive.i64.html#meth... ). Or use wrapping addition ( https://doc.rust-lang.org/stable/std/primitive.i64.html#meth... ) if that was intended.
Note that for the checked case, it is possible to have a newtype wrapper that impls std::ops::Add etc, so that you can continue using the compact `+` etc instead of the cumbersome `.checked_add(...)` etc. For the wrapping case libstd already has such a newtype: std::num::Wrapping.
Also, there is a clippy lint for disallowing `+` etc ( https://rust-lang.github.io/rust-clippy/master/index.html#ar... ), though I assume only the most masochistic people enable it. I actually tried to enable it once for some parsing code where I wanted to enforced checked arithmetic, but it pointlessly triggered on my Checked wrapper (as described in the previous paragraph) so I ended up disabling it.
I partially disagree with this. Using Zig style allocators doesn't really fit with Rust ergonomics, as it would require pretty extensive lifetime annotations. With no_std, you absolutely can roll your own allocation styles, at the price of more manual lifetime annotations.
I do hope though that some library comes along that allows for Zig style collections, with the associated lifetimes... (It's been a bit painful rolling my own local allocator for audio processing).
As long as the type is generic on the allocator, the lifetimes of the allocator don't appear in the type. So eg if your allocator is using a stack array in main then your allocator happens to be backed by `&'a [MaybeUninit<u8>]`, but things like Vec<T, A> instantiated with A = YourAllocator<'a> don't need to be concerned with 'a themselves.
Eg: https://play.rust-lang.org/?version=nightly&mode=debug&editi... do_something_with doesn't need to have any lifetimes from the allocator.
If by Zig-style allocators you specifically mean type-erased allocators, as a way to not have to parameterize everything on A:Allocator, then yes the equivalent in Rust would be a &'a dyn Allocator that has an infectious 'a lifetime parameter instead. Given the choice between an infectious type parameter and infectious lifetime parameter I'd take the former.
Assuming that you are not using much recursion, you can eliminate most of the heap related memory panics by adding limited reservation checks for dynamic data, which is allocated based on user input/external data. You should also use statically sized types whennever possible. They are also faster.
Well on Linux they are apparently supposed to return memory anyway and at some point in the future possibly SEGV your process when you happen to dereference some unrelated pointer.
But for arithmetics Rust has non-aborting bound checking API, if my memory serves.
And that's what I'm trying hard to do in my Rust code f.ex. don't frivolously use `unwrap` or `expect`, ever. And just generally try hard to never use an API that can crash. You can write a few error branches that might never get triggered. It's not the end of the world.
Also there is the no_panic crate, which uses macros to require the compiler to prove that a given function cannot panic.
But on those places, you better know exactly what you are doing.
not sure what the latest is in the space, if I recall there are some subtleties
So, while this is an improvement over C++ (and that is not saying much at all), it's still implemented in a pretty clumsy way.
Doing error handling properly is hard, but it's a lot harder when error types lose information (integer/bool returns) or you can't really tell what errors you might get (exceptions, except for checked exceptions which have their own issues).
Sometimes error handling comes down to "tell the user", where all that info is not ideal. It's too verbose, and that's when you need anyhow.
In other cases where you need details, anyhow is terrible. Instead you want something like thiserror, or just roll your own error type. Then you keep a lot more information, which might allow for better handling. (HttpError or IoError - try a different server? ParseError - maybe a different parse format? etc.)
So I'm not sure it's that Result is clumsy, so much that there are a lot of ways to handle errors. So you have to pick a library to match your use case. That seems acceptable to me?
FWIW, errors not propagating via `?` is entirely a problem on the error type being propagated to. And `?` in closures does work, occasionally with some type annotating required.
If you use `anyhow`, then all you know is that the function may `Err`, but you do not know how - this is no better than calling a function that may `throw` any kind of `Throwable`. Not saying it's bad, it is just not that much different from the error handling in Kotlin or C#.
Better than C, sufficient in most cases if you're writing an app, to be avoided if you're writing a lib. There are alternatives such as `snafu` or `thiserror` that are better if you need to actually catch the error.
No comments yet
[1] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
[2] https://doc.rust-lang.org/std/panic/fn.set_hook.html
[3] https://doc.rust-lang.org/nomicon/panic-handler.html
I like rust, but its not as clean in practice, as you describe
Then implement the `From` trait to map errors from third-party libraries to your own custom Error space:
Now you can convert any result into a single type that you control by transforming the errors: There is a little boilerplate and mapping between error spaces that is required but I don't find it that onerous.Back at Google, it was truly an error handling nirvana because they had StatusOr which makes sure that the error type is just Status, a standardized company-wide type that stills allows significant custom errors that map to standardized error categories.
In any case I will take Rust Result over C++ mess at any time especially given that we have two C++, one with exception support and one without making code incompatible between two.
I could of course create my own type for this, but then it won’t work with the ? operator.
This is what the Try[^1] trait is aiming to solve, but it's not stabilized yet.
[^1]: https://rust-lang.github.io/rfcs/3058-try-trait-v2.html
I could imagine situations where an empty return value would constitute an Error, but in 99% of cases returning None would be better.
Result<Option> may feel clunky, but if I can give one recommendation when it comes to Rust, is that you should not value your own code-aesthetical feelings too much as it will lead to a lot of pain in many cases — work with the grain of the language not against it even if the result does not satisfy you. In this case I'd highly recommend just using Result<Option> and stop worrying about it.
You being able to compose/nest those base types and unwraping or matching them in different sections of your code is a strength not a weakness.
Just need a function that allows lifting option to result.
1: https://news.ycombinator.com/item?id=41543183
it can look just like a more-efficient `except` clauses with all the safety, clarity, and convenience that enums provide.
Here's an example:
* Implementing an error type with enums: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/... * Which derives from a more general error type with even more helpful enums: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/... * then some straightforward handling of the error: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/...
But I hear compiling is too slow.
Is it a serious problem in practice?
Cargo also has good caching out of the box. While cargo is not the best build system, it's an easy to use good system, so you generally get good compile times for development when you edit just one file. This is along made heavy use of with docker workflows like cargo-chef.
Like with any bigger C++ project there's like 3 build tools, two different packaging systems and likely one or even multiple code generators.
Besides developer productivity it can be an issue when you need a critical fix to go out quickly and your pipelines take 60+ minutes.
With some exceptions for core data structures, it seems that if you only modified a few files in a large project the total compilation time would be quick no matter how slow the compiler was.
Rust compile times have been improving over time as the compiler gets incrementally rewritten and optimised.
Is it a serious problem? I'd say 'no', but YMMV.
Granted, there aren't any Rust projects that large yet, but I feel like compilation speeds are something that can be worked around with tooling (distributed build farms, etc.). C++'s lack of safety and a proclivity for "use after free" errors is harder to fix.
https://github.com/abseil/abseil-cpp/blob/master/absl/status...
Why can't I return an integer on error? What's preventing me from writing Rust like C++?
For instance, a common example of the "integer on error" pattern in other languages is `array.index_of(element)`, returning a non-negative index if found or a negative value if not found. In Rust, the return type of `Iterator::position` is instead `Option<usize>`. You can't accidentally forget to check whether it's present. You could still write your own `index_of(&self, element: &T) -> isize /* negative if not found */` if that's your preference.
https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho...
By the way, using "atoi" in a code snippet in 2025 and complaining that it is "not ideal" is, well, not ideal.
Meaning you're in a context where you have control on the C++ code you get to write. In my company, lots of people get to update code without strict guidelines. As a result, the code is going to be complex. I'd rather have a simpler and more restrictive language and I'll always favor Rust projects to C++ ones.
Of course it will probably not be as bad as C++, but still it will be complex and people will be looking for a simpler language.
The conversation function is more language issue. I don’t think there is a simple way of creating a rust equivalent version because C++ has implicit conversions. You could probably create a C++ style turbofish though, parse<uint32_t>([your string]) and have it throw or return std::expected. But you would need to implement that yourself, unless there is some stdlib version I don’t know of.
Don’t conflate language features with library features.
And -Wconversion might be useful for this but I haven’t personally tried it since what Matt is describing with explicit types is the accepted best practice.
I have my gripes with rust, more it’s ecosystem and community that the core language though. I won’t ever say it’s a worse language than C++.
So is this really a language comparison, or what libraries are available for each language platform? If the latter, that's fine. But let's be clear about what the issue is. It's not the language, it's what libraries are included out of the box.
To entertain the argument, though, it may not be a language issue, but it certainly is a selling point for the language (which to me indicates a "language issue") to me if the language takes care of this "library" (or good defaults as I might call them) for you with no additional effort -- including tight compiler and tooling integration. That's not to say Rust always has good defaults, but I think the author's point is that if you compare them apples-to-oranges, it does highlight the different focuses and feature sets.
I'm not a C++ expert by any stretch, so it's certainly a possibility that such a library exists that makes Rust's type system obsolete in this discussion around correctness, but I'm not aware of it. And I would be incredibly surprised if it held its ground in comparison to Rust in every respect!
And while we're at it, why not use assembly? It's all just "syntactic sugar" over bits, doesn't make any difference, right?
Quantity(100) is counterproductive here, as that doesn't narrow the type, it does the opposite, it casts whatever value is given to the type, so even Quantity(100.5) will still work, while just plain 100.5 would have given an error with '-Wconversion'.
Additionally, `clang-tidy` catches this via `bugprone-narrowing-conversions` and your linter will alert if properly configured.
I do run clippy on my Rust projects, but that's a matter of style and readability, not correctness (for the most part!).
I appreciate that there are guardrails in a tool like rust, I also appreciate that sharp tools like c exist, they both have advantages.
Conversions may be fine and even useful in many cases, in this case it isn’t. Converting to std::variant or std::optional are some of those cases that are really nice.
Especially if you're coming from different langs.
It's bad if it alters values (e.g. rounding). Promotion from one number representation to another (as long as it preserves values) isn't bad. This is trickier than it might seem, but Virgil has a good take on this (https://github.com/titzer/virgil/blob/master/doc/tutorial/Nu...). Essentially, it only implicitly promotes values in ways that don't lose numeric information and thus are always reversible.
In the example, Virgil won't let you pass "1000.00" to an integer argument, but will let you pass "100" to the double argument.
Depends on what you want from such a hierarchy, of course, but there is for example an injection i32 -> f64 (and if you consider the i32 operations to be undefined on overflow, then it’s also a homomorphism wrt addition and multiplication). For a more general view, various Schemes’ takes on the “numeric tower” are informative.
https://www.twoscomplement.org
I suppose it could still be added in the future; there are probably several syntax options that would be fully backward-compatible, without even needing a new Rust edition.
So no implicit type conversions, safer strings, etc.
https://dlang.org/spec/betterc.html
A big hard to solve problem is you are likely using a C because of the ecosystem and/or the performance characteristics. Because of the C header/macro situation that becomes just a huge headache. All the sudden you can't bring in, say, boost because the header uses the quirks excluded from your smaller C language.
* "No implicit type conversions" is trivial, and hardly worth mentioning. Trapping on both signed and unsigned overflow is viable but for hash-like code opting in to wrapping is important.
* "Safer strings" means completely different things to different people. Unfortunately, the need to support porting to the new language means there is little we can do by default, given the huge amount of existing code. We can however, add new string types that act relatively uniformly so that the code can be ported incrementally.
* For the particular case of arrays, remember that there are at least 3 different ways to compute its length (sentinel, size, end-pointer). All of these will need proper typing support. Particularly remember functions that take things like `(begin, middle end)`, or `(len, arr1[len], arr2[len])`.
* Support for nontrivial trailing array-or-other datums, and also other kinds of "multiple objects packed within a single allocation", is essential. Again, most attempted replacements fail badly.
* Unions, unfortunately, will require much fixing. Most only need a tag logic (or else replacement with bitcasting), but `sigval` and others like it are fundamentally global in nature.
* `va_list` is also essential to support since it is very widely used.
* The lack of proper C99 floating-point support, even in $CURRENTYEAR, means that compile-to-C implementations will not be able to support it properly either, even if the relevant operations are all properly defined in the new frontend to take an extra "rounding mode" argument. Note that the platform ABI matters here.
* There are quite a few things that macros are used for, but ultimately this probably is a finite set so should be possible to automatically convert with a SMOC.
Failure to provide a good porting story is the #1 mistake most new languages make.
> eventually I came to the depressing conclusion that there’s no way to get a group of C experts — even if they are knowledgable, intelligent, and otherwise reasonable — to agree on the Friendly C dialect. There are just too many variations, each with its own set of performance tradeoffs, for consensus to be possible.
The core of Rust is actually very simple: Struct, Enum, Functions, Traits.
Safer strings is harder, as it gets into the general memory safety problem, but people have tried adding safer variants of all the classic functions, and warnings around them.
Sun actually did it right with Java, recognizing that if they mainly targeted SunOS/Solaris, no one would use it. And even though Oracle owns it now, it's not really feasible for them to make it proprietary.
Apple didn't care about other platforms (as usual) for quite a long time in Swift's history. Microsoft was for years actively hostile toward attempts to run .NET programs on platforms other than Windows. Regardless of Apple's or MS's current stance, I can't see myself ever bothering with Swift or C#/F#/etc. There are too many other great choices with broad platform and community support, that aren't closely tied to a corporation.
Aliasing rules can also be problematic in some circumstances (but also beneficial for compiler optimisations).
And the orphan rule is also quite restrictive for adapting imported types, if you're coming from an interpreted language.
https://loglog.games/blog/leaving-rust-gamedev/ sums up the main issues nicely tbh.
I bet assembly programmers said the same about C!
Every language has relatively minor issues like these. Seriously pick a language and I can make a similar list. For C it will be a very long list!
Anyhow, I won't go back to C++ land. Better this than whatever arcane, 1000-line, template-hell error message that kept me fed when I was there.
Even Rust's types aren't going to help you if two arguments simply have the same types.
Or another (dummy) example transfer(accountA, accountB). Make two types that wrap the same type but one being a TargetAccount and the other SourceAccount.
Use the type system to help you, don’t fight it.
Sound type systems are equivalent to proof systems.
You can use them to design data structures where their mere eventual existence guarantee the coherence and validity of your program’s state.
The basic example is “Fin n” that carries at compile time the proof that you made the necessary bounds checks at runtime or by construction that you never exceeded some bound.
Some languages allow you to build entire type level state machines! (eg. to represent these transactions and transitions)
My median Rust coding session isn't much different, I also write code that doesn't work, but it's caught by the compiler. Now, most people call this "fighting with the borrow checker" but I call it "avoiding segfaults before they happen" because when I finally get through the compiler my code usually "just works". It's that magical property Haskell has, Rust also has it to a large extent.
So then what's different about Rust vs. C++? Well Rust actually provides me a path to get to a working program whereas C++ just leaves me with an error message and a treasure map.
What this means is that although I'm a bad programmer, Rust gives me the support I need to build quite large programs on my own. And that extends to the crate ecosystem as well, where they make it very easy to build and link third party libraries, whereas with C++ ld just tells you that it had a problem and you're left on your own to figure out exactly what.
There are some places I won’t be excited to use rust, and media heavy code is one of those places…
And don't get me started on dynamic graphs.
I would happily use Rust over C++ if it had all other improvements but similar memory management. I am completely unproductive with Rust model.
And while unsafe Rust does have some gotchas that vanilla modern C++ does not, I would much rather have a 99% memory-safe code base in Rust than a 100% "who knows" code base in C++.
You gotta get your timing right. Parry, parry, and then stab.
But once you are _maintaining_ applications, man it really does feel like absolute magic. It's amazing how worry-free it feels in many respects.
Plus, once you do embrace it, become familiar, and start forward-thinking about these things, especially in areas that aren't every-nanosecond-counts performance-wise and can simply `Arc<>` and `.clone()` where you need to, it is really quite lovely and you do dramatically less fighting.
Rust is still missing a lot of features that other more-modern languages have, no doubt, but it's been a great ride in my experience.
The idea with Rust is that you get safety...not that you get safety at the cost of performance. The language forces you into paying a performance cost for using patterns when it is relatively easy for a human to reason about safety (imo).
You can use `unsafe` but you naturally ask yourself why I am using Rust (not rational, but true). You can use lifetimes but, personally, every time I have tried to use them I haven't been able to indicate to the compiler that my code is actually safe.
In particular, the protections for double-free and free before use are extremely limiting, and it is possible to reason about these particular bugs in other ways (i.e. defer in Go and Zig) in a way that doesn't force you to change the way you code.
Rust is good in many ways but the specific problem mentioned at the top of this chain is a big issue. Just saying: don't use this type of data structure unless you pay performance cost isn't an actual solution to the problem. The problem with Rust is that it tries to force safety but doesn't have good ways for devs to tell the compiler code is safe...that is a fundamental weakness.
I use Rust quite a bit, it isn't a terrible language and is worth learning but these are big issues. I would have reservations using the language in my own company, rather than someone else's, and if I need to manage memory then I would look elsewhere atm. Due to the size of the community, it is very hard not to use Rust too (for example, Zig is great...but no-one uses it).
The pragmatism of Rust means that you can use reference counting if it suits your use case.
Unsafe also doesn't mean throwing out the Rustiness of Rust, but others have written more extensively about that and I have no personal experience with it.
> The problem with Rust is that it tries to force safety but doesn't have good ways for devs to tell the compiler code is safe...that is a fundamental weakness.
My understanding is that this is the purpose of unsafe, but again, I can't argue against these points from a standpoint of experience, having stuck pretty strictly to safe Rust.
Definitely agree that there are issues with the language, no argument there! So do the maintainers!
> if I need to manage memory then I would look elsewhere atm
Haha I have the exact opposite feeling! I wouldn't try to manage memory any other way, and I'm guessing it's because memory management is more intuitive and well understood by you than by me. I'm lazy and very much like having the compiler do the bulk of the thinking for me. I'm also happy that Rust allows for folks like me to pay a little performance cost and do things a little bit easier while maintaining correctness. For the turbo-coders out there that want the speed and the correctness, Rust has the capability, but depending on your use case (like linked lists) it can definitely be more difficult to express correctness to the compiler.
I think the issue that people have is that they come into Rust with the expectation that these problems are actually solved. As I said, it would be nice if lifetimes weren't so impossible to use.
The compiler isn't doing the thinking if you have to change your code so the compiler is happy. The problem with Rust is too much thinking: you try something, compiler complains, what is the issue here, can i try this, still complain, what about this, etc. There are specific categories of bugs that Rust is trying to fix that don't require the changes that Rust requires in order to ensure correctness...if you use reference counter, you can have more bugs.
https://play.rust-lang.org/?version=stable&mode=debug&editio... https://play.rust-lang.org/?version=stable&mode=debug&editio...
By default, they're on during debug mode and off in release mode.