Kotlin native vs Rust

Returning error codes is how things used to be done. It’s simple for a compiler to implement, and very explicit, but doesn’t scale. (The point where the error code is returned is often not the right place to handle the error — it’s at the wrong level of abstraction — so you need to pass error codes around all over the place.)

Worse, while it enables safe code, it does little to encourage it. In practice, most error codes were ignored. (Who checks the return code from printf(), for example? Just about no-one!)

That’s why exceptions were developed: they’re an improvement in general. You can handle the error at the right place, without polluting all the intervening code; you can store lots of info in the exception, to aid handling and debugging; and related exceptions can be handled the same. And if you don’t do anything, then the program halts — it ‘fails safe’, unlike the explicit case where doing nothing means it ignores the error and ploughs happily on, which can do far more damage.

Of course, they’re not best suited to every possible situation. But their benefits aren’t as subjective as all that.

1 Like

I think you’re comparing C’s error handling with something totally different. And again, unless you’ve used it in a real-world project, this is all speculation.
You don’t have to use Rust for that, in fact this is not unique to Rust at all. Vlang is one example among many, and functional languages usually have the same patterns.

Worse, while it enables safe code, it does little to encourage it. In practice, most error codes were ignored. (Who checks the return code from printf(), for example? Just about no-one!)

I don’t know, but you can’t ignore it in Rust. That’s the point. Errors are not codes either, they can be any type and hold data.
Option and Result are union types. They’re literally defined like this:

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

If a function gives you an optional (Option), that’s exactly like Kotlin’s nullable values. You must handle the None (equivalent to null) case. Ignoring None in this sense is basically calling unwrap(), which gives you the result if it’s Some, or panics if it’s None, !! is the equivalent of unwrap in Kotlin (although usually with smart casts Kotlin can know if you’ve checked it before).

If it returns a Result, you can do the same as well to “ignore” an error.
Using unwrap() means you’re prototyping and probably do not care much, or you’ve already checked the invariants and are sure this will never be a None or Err (if it is, it’ll panic, signaling a bug).

it ‘fails safe’, unlike the explicit case where doing nothing means it ignores the error and ploughs happily on, which can do far more damage.

It panics.

I probably said this 20 times now. Ignoring means panicking, which is Rust lingo for crashing and burning everything down.
Again, in 99% of cases, using unwind runtime (which is default), this can be caught exactly like exceptions. The failing code will not be rescued or resumed (otherwise we’re back to C, what’s the point of Rust?), but you can still save the rest of your program and handle this accordingly. But catch_unwind is there for very niche cases, otherwise everything follows the Option and Result patterns.

But wait…

I don’t know why are we having an argument about Rust’s error handling. It’s nothing new, functional languages have always done it and it proved itself more reliable than exceptions. That doesn’t mean exceptions are bad, they’re still a whole lot better than error codes that can be happily ignored or misinterpreted while continuing to execute the code (leading to corrupted data, security vulnerabilies… hell, even nazal demons) and provide very little information.

I never thought I’d have to argue with anyone about Rust’s error handling nor the promise of zero cost abstractions, because anyone who took the language seriously (even slightly) will know that these are the actual pros of the language.

I’m not a fanboy of anything, I love exploring new ideas and languages, Rust is only one of them. But truth has to be said. Rust does have issues that actual users do know. We love the borrow checker but it can be annoying. Fearless concurrency is a real thing, but if you screw up… your code won’t build and compiler errors can be unwieldy and hard to understand (which is good… but pretty annoying). Dynamic dispatch still has tons of room for improvements, many parts of async support are still unstable, traits can and should be able to do more, many limits can be lifted, and a lot more.

All of these things are being worked on to make them easier, more approachable, and more predictable, because this isn’t C or C++, and changes are not too scary for the language (yet, they still don’t break old crates, through the “editions” feature).

What happens if it panics, can you catch it?
The problem is then to handle two ways of errors, those which are returned and those which will be thrown.

A possible scenario for not handling result types is when your result is in fact stored in the mutable reference and the return type is either error or just okay.
Then you can possibly ignore the error.

I have to admit that Rust is probably the better choice most of the time than C++, however you can’t really do write performant Rust code without being unsafe (e.g. linked lists, doubly linked lists, was my first bad experience with this language),
Traversing the stdlib also shows many cases of unsafe fragments.
Therefore, arguing Rust is safe and lets you write performant code as C++ at the same time is a bit cheating.

I would still favor for it, though I don’t like unsafe being semi safe, would rather prefer unsafe as unsafe to have the most freedom.

What happens if it panics, can you catch it?

Have you actually learnt the language?
All panics are explicit in code. You choose to panic. If you don’t want to panic, handle the error.

Also, the data you ask from the function is owned by the Option / Result, and combined with ownership and move semantics it enables some powerful patterns where you can never make a mistake. If you call a function that opens a database, the handle to that database will be inside the Result, you can only touch it or operate on it after you call unwrap, otherwise the variable is useless (so you’ll get a warning for unused variable) or you put it nowhere (so you get a warning about unused Result).
Ownership makes this even better, because while most methods take references (&self), you can make the close() method take the value (self) which will move the value to that function, making it impossible to use it after that (otherwise compiler error). I find this property awesome, and the only other languages to have similar abilities seems to be Val and Vale, two new incomplete languages (ironically with similar names lol).

When we talk about Rust being all about correctness, this is what we talk about.

I was gonna write example code, but why should I make this effort when all of this is explained in the official book? Please take some time to learn it.

So let me repeat:

Error handling in Rust is a non-issue. No one talks about it because it simply is not an issue.

I don’t like unsafe being semi safe

The unsafe keyword doesn’t mean “this code is unsafe and it will cause nazal demons”. It simply means “you are in control, make sure to get everything correct since there won’t be much help from the compiler.”

Having unsafe code in stdlib is normal, and sometimes using it in other contexts is also fine. The best thing about it is the drum that every Rust developer keeps beating: local reasoning.
Most unsafe parts in Rust’s std are very small and self-contained, have no side-effects, and do one tiny thing correctly in a very obvious proven-to-be-safe way. It’s not like we write full-blown complex algorithms in unsafe blocks. Remember, C is inherently unsafe, yet developers write tons of safe code in C, when they do it correctly. Rust isolates the cases when you really need unsafe, in small, contained blocks that you can reason about easily.

The stdlib is safe, and aside from the fact that its unsafe parts are safe and checked, they’re all also extensively tested through fuzzing and other detection tools that can find possible UB during execution.
Writing safe wrappers over unsafe code is not new either.

Also, std doesn’t use unsafe for performance. It uses it when it does something the borrow checker wouldn’t allow. For performance, using higher-level constructs usually yields better performance than a writing a low-level implementation manually, because you don’t know as many tricks and avenues as the compiler does, and using higher-level features means you tell the compiler what you’re trying to do, not how you’re tying to do it, and it almost always knows better.
It’s similar to the misconception that manually writing assembly will be faster than equivalent C / Rust code. Their compilers will brutally beat you to death when you run the benchmarks.

However:

Practically speaking, you will never need to use unsafe. ever. Anything you can think of probably already exists in safe form in stdlib, or in famous crates that are checked by Rust’s team and fuzzing tools.

LinkedList?

You won’t need it in 99% of the time. Unless you’ve tried using a Vec or other array-based data structures and they didn’t work for you, you’re probably choosing the wrong thing. In fact, it still exists in Rust’s stdlib through sheer luck, as they already kicked out most other similar structures to external crates before 1.0, but LinkedList slid through.
So yeah, there’s a built-in implementation of LinkedList in std so you don’t need to do it yourself. It uses quite a bit of unsafe internally because LinkedLists are just really wonky :stuck_out_tongue: But it’s safe to use nevertheless since the code is not magic either.
For any other well-known data structure, there are famous well-known crates that are battle-tested doing it for you (unless you’re a kernel/bare-metal developer needing to optimize something to your very specific use-case, but then the conversation would be at a completely different level).

Again, there aren’t that many complaints about unsafe in Rust. If you don’t use it in your code base, you have no UB. Period. 99% of Rust devs never touch unsafe, yet write performant code.

Most of this discussion is well over my head, lol. But I’ll chime in with my two cents anyway.

My first experience of Rust was doing a basic tutorial to read input from a user and then print it, or something like that. I didn’t really like the syntax much (lots of double colons, I didn’t understand why), but the thing that made me NOPE out of there was the way reading input is handled. read_line returns a Result; fair enough, Rust is using functional programming concepts where (almost) every function returns a monad, and you have to handle the success and failure cases. But where it lost me was that the returned Result’s success value is the amount of bytes read in, while the actual text is added to the mutable String variable that you passed into the function. I’m a big fan of immutability, so mutating variables never sits well with me, but using and mutating parameters to return data from a function? To me, that’s one of the biggest anti-patterns in programming. Parameters are input, return value is output.

Maybe this is the only part of Rust that does that, and the rest of the language avoids mutating parameters, but that short experience was enough to make me not want to learn more.

1 Like

I feel you here. I also don’t like the read_line method syntax in Rust very much. Even if you want to write a ‘press-key-to-progress’ block you still need to pass an empty String as input.

2 Likes

I don’t know Rust and I agree with you about returning results through parameters. I just wanted to point out, we can’t entirely avoid this pattern in the case of IO, because for performance reasons we often like to reuse buffers. It feels bad to use this pattern for something like read_line, but again, I don’t know Rust, so I don’t know if this is justified or not.

edit:
But if it doesn’t reuse a buffer, but allocate a new one and set our reference to it, then well, I can’t defend this approach :slight_smile:

2 Likes

Agree! I don’t really like using that pattern. In fact I’ve always wrapped it in a separate function just like this comment on a thread.

Apparently it was initially like that, but they changed it before 1.0 as cited in the same thread. Afaik it seems based on the grounds of performance and basically IO complexities just like @broot mentioned (whose details are well over my head lol). Otherwise, this pattern is rarely used.

That last sentence doesn’t sound good though :sweat_smile: If you leave at every small mishap you’ll never learn anything!

apparently the designer of fungus, Graydon Hoare left and went on to work on Swift at Apple !

I take your point. :stuck_out_tongue: But at the same time, the only reason I was checking out Rust was simple curiosity; a few years ago, a coworker of mine was getting into it, so I decided to check it out to see what was all about. I didn’t have a specific problem I was trying to solve with it, nor a situation where I believed it was the best tool for the job, so when my first impressions weren’t good, I left it at that. I’m perfectly happy with Kotlin.

If I had a specific use case where a lot of people were telling me Rust is the best solution, then I’d stick with it more, but for a simple “what is this language all about?”, I didn’t see the need to pursue it when I didn’t like its patterns. :slight_smile:

For what it’s worth, that I/O pattern of passing in an output buffer is very familiar to Rust’s target audience. We’ve been doing that in C and C++ forever, and it’s important when performance matters, for minimizing memory allocations and copying. Java uses it too, for the low-level operations.

I see, that’s totally okay! My whole gripe in this thread is with people who have no use for Rust, never used it, yet talk about things that nobody complains about.
I also initially learnt Rust out of curiosity but here I am now writing a desktop app for store terminals with it and GTK4 in my first job (since we needed low memory overhead), and my experience was (generally) pretty good. Otherwise, Kotlin rocks and I love using it so much more, especially with Jetpack Compose!

I learned Rust for 2 weeks, Go for 2 weeks and here is my wish:

  • If Kotlin Native = Rust - Rust's borrow checker + Go's light GC, I would use Kotlin Native for 95% of cases everywhere: distributed systems, system programming, backend services, cloud services, frontend, competitive programing, etc.
  • Other 5% is for extreme cases. For example, when peak performance and safety is a must, I would use Rust.

Unfortunately, that’s only a wish.
I cannot use Kotlin JVM for Competitive Programming because of TLE.
I cannot use Kotlin Native for Competitive Programming. I’m not sure why all platforms only support Kotlin JVM.
I must use Go for almost distributed systems.
I must mostly use C++ or Rust for system programming.
I miss Kotlin a lot. I wish one day, Kotlin Native is 100% native and find a sweet spot to replace Rust and Go for 95% of cases. Then Kotlin Native will become the mainstream, not Kotlin JVM. I really don’t like the fact that Kotlin JVM is holding Kotlin Native back so that the native version cannot live and grow independently.

1 Like

I kinda agree there. Some Rust features are really in-line with Kotlin’s philosophy, but they cause issues with JVM interop so they’ll probably never make the cut. However even with JVM restrictions I think there’s still a lot more room to grow, and Kotlin/Native now has a GC very similar to Go.

Just seeing Kotlin/Native being used for stuff like gtk-kn (GTK bindings for Kotlin/Native) really shows that it’s in the right track, and it’ll hopefully get even better after K2.

I really don’t like the fact that Kotlin JVM is holding Kotlin Native back so that the native version cannot live and grow independently.

This is impossible as long as Kotlin/Native doesn’t get enough attention from Jetbrains and the community, and most of the community are ‘Java’ developers, look at the Kotlin/Native targets and you will know that Kotlin/Native was created in the first place only for iOS, even after all this time Linux and Windows targets are Tier 2 ( Windows is Tier 3 actually. ), unlike iOS which is Tier 1, and if you looked at Compose multiplatform, it targets the ‘JVM’ for desktop applications and ‘Native’ for iOS, again Kotlin/Native is not a Kotlin/Native is Kotlin/iOS.
That is why Compose multiplatform will be limited to Android and iOS applications perhaps, and I would prefer to use Flutter instead, it actually ‘Native’ and I do not have to use the ‘JVM’ in this modern era, and Dart 3.2 now does have support for interop with Java/Kotlin and Objective-C/Swift, of course maybe after sometime Compose multiplatform is going to support the Native target for Linux and Windows, but Kotlin is not the only one improving with time.

2 Likes

Just seeing Kotlin/Native being used for stuff like gtk-kn (GTK bindings for Kotlin/Native) really shows that it’s in the right track, and it’ll hopefully get even better after K2.

Maybe if Jetbrains gives it more attention and stops focus so mush on the ‘JVM’ because by doing that they are tying the future of the language to Java, and Kotlin will suffer the same fate as Scala and Groovy. It is hard to convince non-Java programmer to learn Kotlin instead of Go or Rust for example,because now or later he will have to deal with Java.

2 Likes

Maybe if Jetbrains gives it more attention and stops focus so mush on the ‘JVM’ because by doing that they are tying the future of the language to Java, and Kotlin will suffer the same fate as Scala and Groovy.

Completely agree, at least it will absolutely happen to me personally.

I am waiting for these 3 production releases: Kotlin/Native, Zig, Carbon.
Not sure if something else will appear to this competition, still watching.
Only the future can answer, what will be the next modern native programming languages people love.
If I can find something native, modern and productive, I am sure I am not going to use JVM-dependent languages for my future projects. Java, Kotlin/JVM, Spring Boot will only be there for my legacy projects.

1 Like

Is Kotlin/Native still beta already?

No, Kotlin/Native is stable.

As far as I know, Kotlin/Native is only partially production ready. It is not always available.

Not 100% relevant, but I think somehow important to CP community: no CP platforms support Kotlin/Native yet. They only support Kotlin/JVM. And you will get TLE/MLE in some problems, even if you solution is optimal and using all possible tricks to get faster. Converting to Go or C++ using exactly the same solution to get AC.