DEV Community

Cover image for So I tried Rust for the first time.

So I tried Rust for the first time.

Martin Häusler on June 04, 2024

In my professional life, I'm at home on the Java Virtual Machine. I started out in Java over 10 years ago, but since around 4 years, I'm programmin...
Collapse
 
esfo profile image
Esfo • Edited

I believe the specification of the type is required because collect is not specific to Vecs but it can collect into any type that implements FromIterator

Collapse
 
martinhaeusler profile image
Martin Häusler

That's interesting, I didn't know that! I think it's crazy to determine a method overload based on its return type.

Collapse
 
esfo profile image
Esfo

Its generic trait magic actually, not much overloading in rust, the collect method is just implemented for all types which implement the fromiterator trait

Thread Thread
 
martinhaeusler profile image
Martin Häusler

Sorry, that was my Java lingo coming through ^_^'

What I mean is: the compiler has the choice between multiple functions:

  • fn collect(&self) -> Vec<T>
  • fn collect(&self) -> Set<T>
  • ...

which all have the same name and the same &self, they are different only in their return types. In the Java / Kotlin world, this is strictly forbidden and the compiler will not permit it.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

That's because Java/Kotlin can't actually distinguish such methods. Rust expands generics for every combination of types, so it can use inference using return type too.

Thread Thread
 
martinhaeusler profile image
Martin Häusler

... and lose the ability to infer the type of the variable which will hold the value in the process. From my perspective it's not a good language design choice because you have multiple methods, that take the same arguments, but can do vastly different things just based on the type you want them to return to you. And it's not just my Java background; C doesn't allow you to do that either. Or Kotlin. Or C#. Or Go (as far as I know). I'm a proponent of overloading where it makes sense, but distinguishing on return type alone is a risky move.

Thread Thread
 
siy profile image
Sergiy Yevtushenko • Edited

It's not the same overloading you're used to. Don't judge Rust from OO standpoint because it is not an OO language. Rust is rather a quite specific hybrid language where OO not even a significant part. Actually, only the dyn traits somehow belong to OO and generates a traditional dynamic dispatch call. Everything else is rather a specific subset of functional languages, starting from error/null handling, sum types (enums in Rust parlance) and type classes (aka traits).

Thread Thread
 
martinhaeusler profile image
Martin Häusler

Overloading has nothing to do with OO. I can overload functions in C. Or Haskell. It's just how a compiler determines which function to call if there are multiple options that share the same function name. It's not even polymorphism and it has nothing to do with dynamic dispatch. Just functions sharing the same name, and the compiler has to figure out which one it is. And it feels weird to me that the expected return type is part of this resolution, that's all I'm saying.

Thread Thread
 
martinhaeusler profile image
Martin Häusler

I'm questioning if this is a good choice. So far, I don't think so. That may change over time, let's see.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

I'm just trying to say that there is nothing weird with that in Rust because it is not one of the languages you're used to.

Thread Thread
 
laundmo profile image
laundmo

I dont think you quite understand yet what collect does. Its not overloaded as such.

FromIterator is a Trait (interface) which collection types can implement to define how they are created from a iterator. Vec, HashMap and String implement this. All collect does is call FromIterator corresponding to the return type. Essentially it calls Vec::from_iter(&self).

This is quite a bit different from overloading as the collectiob type itself is where the logic lives, not Iterator::collect.

Collapse
 
pgradot profile image
Pierre Gradot

Gods why. That's all I have to say. Why does the darn thing refuse to die? Chalk up another language with mandatory semicolons, they just keep coming.

Well, it goes even further: it's possible for a line not to finish with a semicolon. That's because
Rust is expression-based and semicolons are for statements.

Compare these equivalent functions:

fn square_with_statement(n: i32) -> i32 {
    return n*n;
}

fn square_with_expression(n: i32) -> i32 {
    n*n // no ; here
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
martinhaeusler profile image
Martin Häusler

Yup I noticed that when writing lambdas. So we sometimes can work without semicolons in rust. If we compare it to Kotlin, you only have to use them if you have multiple statements on the same line (which nobody ever really does). It's a blessing.

Collapse
 
siy profile image
Sergiy Yevtushenko

Do you write text without dots at the end of sentences? Are dots annoying you too?

P.S. Just yet another useless Kotlin "feature". I really can't understand why Kotlin fans are so excited about it, especially because it causes harm to other Kotlin "feature" - infix methods.

Thread Thread
 
martinhaeusler profile image
Martin Häusler

That comparison makes no sense at all, because sentences in natural language are structured differently, and we don't restrict ourselves to one sentence per line. My point was: other languages have demonstrated that semicolons are not necessary for a language to be parsed. So why bother with them? Do they make the statement more readable? Hardly. I'd argue our programmer eyes are just used to look past them. But we still have to type them. Program syntax should primarily be easy for humans to read.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

The elimination of semicolons has zero value. In the case of Kotlin, it even causes harm. So, even if some language designers decided that semicolons are not necessary, it does not mean that everybody should follow them. Especially given two facts:

  • Rust and Kotlin did arrive on the scene in about the same time frame, so they don't have to be similar in any way
  • Rust makes reasonable use of semicolons and their absence

And yes, program syntax should be easy to read, but semicolon does not affect this as significantly (and badly) as, for example, post-typing and unavoidable noise caused by it.

Thread Thread
 
pgradot profile image
Pierre Gradot

Do you write text without dots at the end of sentences
Are dots annoying you too

P.S. Just yet another useless Kotlin "feature"
I really can't understand why Kotlin fans are so excited about it, especially because it causes harm to other Kotlin "feature" - infix methods

Just like that.

You don't speak with dots. Dots are just a way to separate sentences when we write. We could have used many other techniques. New lines could work well too.

To get back to code, semicolon may not be problematic. But are they really convenient? Why keeping them if we can work without them?

Thread Thread
 
siy profile image
Sergiy Yevtushenko

Dots represent in the writing the way we actually speak. Each sentence is spoken with intonation, where for dot there is a specific intonation and the pause. So we DO speak with dots, actually.

Not semicolons or lack of them is the root of the issue. It's just ridiculous to see Kotlin fans praising the optionality of semicolons given that "post-typing" syntax adopted by Kotlin uses a lot of other syntactic noise - colons, arrows and "fun". If the existence of semicolons could be at least somehow justified, mentioned above noise is necessary only to the compiler, they have no meaning/value for a human. Moreover, Go perfectly fine eliminated colons, so it's also possible to work without them. Why no Kotlin fans complain about their presence?

Thread Thread
 
pgradot profile image
Pierre Gradot

When you're a kid, we speak before we write. We speak with pauses and intonations. We don't speak with dots. Dots are just a way to represent them. Other languages and others forms of writing use different ways. The beginning of Wikipedia article about Chinese punctuation or this one about upside-down question mark in Spanish are interesting.

But I believe we are getting far away from Rust, aren't we ? ;)

I won't say anything about Kotlin since I haven't try this language yet.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

When you're a kid, we speak before we write. We speak with pauses and intonations. We don't speak with dots. Dots are just a way to represent them.

Yes, dots are the way to represent pauses and intonations. We don't speak with characters, either. Characters, just a way to represent phonemes. Some languages use other representations for the words and don't use characters. So?

Frankly, if you skip Kotlin, you wouldn't lose anything.

Collapse
 
pgradot profile image
Pierre Gradot

I have been using Python a lot for the last 12 years, and what I like most about this language is the absence of braces and semicolons XD

Collapse
 
siy profile image
Sergiy Yevtushenko
  • Syntax is a lot like Kotlin, see no reasons for complaining from your side :)
  • Macros are good and exceptionally useful for elimination of repetitive code and doing many other useful things.
  • Rust has an excellent error handling. It's clearly visible in the code (unlike exceptions) and prevents accidental skipping of error handling.
  • It's not that steep, especially given how helpful the compiler is
  • Type inference works in all cases, you just hit the case which is impossible in Kotlin

Kotlin "it" is an ugly hack, which strips valuable context. The lack of mandatory semicolons makes it impossible to reformat code with infix functions inside because the resulting code wouldn't compile. And, frankly, "effectiveness" and "productivity" of Kotlin is a marketing myth. It's a poorly designed and thought out language, with inconsistencies and mental overhead striking from every corner.

Collapse
 
martinhaeusler profile image
Martin Häusler

I write Kotlin every day with my team in my professional life, and especially if you compare it to Java, the productivity is not a marketing myth at all. I don't know what kind of negative experiences you've had with the language, but I can assure you I've also had trouble in the beginning until it "clicks" and you see how things roll, and then it far surpasses Java. As for rust, I haven't hit that "click" point yet, but I'm not giving up :)

Collapse
 
siy profile image
Sergiy Yevtushenko

My first Kotlin project was in 2018, so I have some experience with it. And it has zero advantages over Java, especially if Java code uses functional style error/null handling, which actually improves productivity (unlike Kotlin) and code quality.

Thread Thread
 
martinhaeusler profile image
Martin Häusler

Nothing in Java prevents anyone from returning null from a method that has return type Optional. The compiler doesn't care, it's smoke and mirrors, good will and developer discipline but there is zero safety associated with it. Expressing nullable fields isn't solved by it either since the authors themselves only recommend Optional for return types. Sure, you can do the functional stuff in Java, the same way you can do OOP in C, but in both cases the compiler won't help you. Java has never been null safe and never will be. Kotlin is, by default. The only real alternative you have in Java is to rely on annotation based build chain processors (Lombok, Checkerframework, ...) and pray that nobody compiles your code without them, but why bother when the alternative is so easy and just straight up better? Kotlin has its flaws and I do have my pet peeves with it, but none of the things you've listed I would count among them. I've never looked back to Java since 2018, not even once. YMMV of course.

Thread Thread
 
siy profile image
Sergiy Yevtushenko
  • Nothing prevents, but a simple convention works quite well. It's very easy to stop using explicit null. Records and fluent builder pattern helping with elimination of default initialization issues. Lack of null checks virtually immediately exposes any remaining null issues during testing. By the way, in this regard it's a lot like Scala, which also relies on convention and nobody complains about lack of null safety.
  • Compiler helps as soon as one starts using Result and Option types. And no, this is not the same as OOP in C.
  • Annotation processing is a fig leaf which does not work.

Java 21 is a significantly different language than Java 8. Basically, Kotlin has no features which would justify switching to it from Java 21.

Thread Thread
 
martinhaeusler profile image
Martin Häusler • Edited

Data classes (records are incompatible with the bean definition and are always immutable), extension methods, context receivers, val instead of final var, compiler-checked null safety, better operators for null handling (?:, ?.), multiple compilation targets outside the JVM, if and try-catch as expressions, it in lambdas, inline methods, proper string templates without dumb prefixes, better visibility defaults (classes and methods are public, fields are private)... oh boy I could go on. I think the JVM is a great piece of engineering, really. But Java as a language feels very dated at this point.

Thread Thread
 
siy profile image
Sergiy Yevtushenko
  • Data classes are unnecessary
  • Extension methods "Kotlin style" are harmful, but if one needs them, they are available via Lombok and Manifold annotation processors. No wide use so far, BTW.
  • Optional chaining is unnecessary and not composable. Elvis operator may have non-obvious behavior. Nothing of these is necessary for functional style.
  • Native target is present (GraalVM)
  • ternary operator replaces if as expression. try-catch is unnecessary for functional style
  • it strips context and is harmful
  • inline/noinline/crossinline are signs of poor language design rather than something useful
  • String templates are preview feature and will change in next previews
  • Visibility defaults is not a feature

Yes, you can go on and list every single Kotlin feature, but you still be unable to name any really significant feature which would justify switching to Kotlin. And if you think that Java is dated, then you don't know it anymore. I'm not surprised, actually, Kotlin even deeply ties code to a rusty imperative style. Just with some useless fancy features and noisy syntax.

Thread Thread
 
martinhaeusler profile image
Martin Häusler

Calling every feature "harmful" or "unnecessary" without any context is a poor basis for discussion. You do you then, each to their own. But if you want to go full functional so badly, why not straight up Haskell? Java seems like a really odd choice for that.

Thread Thread
 
siy profile image
Sergiy Yevtushenko • Edited
  • it is harmful because, in general case, variables/parameters should hold context, i.e. refer to some domain element. There could be exceptions, but they are exceptions, not a norm. Kotlin makes it a norm for the sake of brevity.
  • I didn't say anything about "go full functional". I'm referring only to a narrow functional toolset for handling so-called special states of values - missing value, error or value, not-yet-available value. And Java is perfectly fine for this approach.
Collapse
 
bbkr profile image
Paweł bbkr Pabian

I'm also learning Rust and I immediately loved Result and Option enums. The clarity of function interfaces and error handling is amazing. The more you write Rust the more you realize how try/catch has always been a false friend.

But to fully appreciate it you must learn also idiomatic paths. How to take full advantage of advanced pattern matching, how to convert between Option and Result, how to "rethrow" different Err to upper layers, etc.

Collapse
 
bbkr profile image
Paweł bbkr Pabian

Macros are all about performance. If you can unroll println!("foo {} bar", thing) macro in compile time into executable code "foo " + thing + " bar" you will save A LOT. For example in Raku language rewriting printf patterns into AST gave lately 100x speed boost compared to runtime interpolation.

Collapse
 
siph profile image
Chris Dawkins

There are lots of ways to handle errors in rust, I highly recommend reading the rust book. Also, you only briefly mentioned this but fun is way better than fn and changing back-and-forth between them is agonizing.

Collapse
 
martinhaeusler profile image
Martin Häusler

Well the rust book in its chapter on error handling (doc.rust-lang.org/book/ch09-00-err...) mentions two ways:

  • either panic!
  • or use Result

Which is a valid approach, but I still miss my trusty try-catch. And I feel your pain regarding fn vs. fun :D

Collapse
 
siy profile image
Sergiy Yevtushenko

Rust employs functional style error (and null) handling, and this is one of the reasons for Rust code to be so reliable and efficient. But beside efficiency and reliability, this error handling style improves productivity and eliminates a lot of mental overhead.

Exceptions (especially unchecked ones, and Kotlin has no other exceptions) are stripping important information - that function/method may return an error. So, quite often one needs to navigate to invoked code and check if the exception is actually thrown. Sometimes it is necessary to look deeper because the method invokes other methods which may throw an exception. This causes significant distraction because it requires developers to read tons of unrelated code. And still does not guarantee that he/she didn't miss an exception. With functional error handling, there is no such issue. You clearly see that error may happen, and the compiler ensures that you either, handle it or propagate it. Same with nulls aka Option. Notice, how "double" Kotlin's type system turns into useless garbage with this approach. And one of the most elegant Rust implementation details is that None at the binary level is the same as C NULL.

P.S. "Post-typing" is trash, no matter if this is Kotlin, Rust, Go or whatever. It makes syntax inherently polluted with "fun"/"fn", colons, arrows and other noise. But the worst thing is how much it complicates reading of the function signature because one of the key elements - return type - is hidden and one needs to dig through the whole signature to find it.

Thread Thread
 
martinhaeusler profile image
Martin Häusler

Except that rust has panic. And unwind. So basically it boils down to the same thing again. Why? Because including every error condition, unlikely as it may be, in the signature of a function is simply not practical. Checked exceptions in Java were one of its worst design choices, which is why people abandoned them; some ancient Java 1.2 core libraries aside, you'll not find many checked exceptions in the wild. Take REST APIs for example. It doesn't make sense for me to treat a failure in my database call. The application as a whole can recover from that, but the request certainly cannot. So I let it fly, the REST framework catches it and converts it into an appropriate HTTP response while I have my hands free to work on the thing the endpoint actually needs to do.

I agree that there is some merit to error types. But in most cases, the actual content of the error doesn't matter all that much; something went wrong. Kotlin uses nullable return types for this, and since the language is inherently null safe, you are forced to deal with the null value. But the language syntax gives you the necessary tools for it as well. Rust does something kind of similar with Result<T,E> and the ? operator, except that it is very convoluted to write in the return types. Everything gets boxed in several layers of containers, which hides the true intention of the code.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

You're missing the key point: panic and unwind are unrecoverable. This is how unchecked exceptions should be used in practice, but they don't.
No, checked exceptions were not the worst design choice. It was an attempt to preserve an essential context. Unfortunately, this approach results in deep coupling and this is why it was largely abandoned. But note that Java was basically the first language where authors realized the existence of the issue.
Yes, there are not so many checked exceptions in the wild, but instead we got technical unchecked exceptions as means to transfer business error information. Frankly, I don't know what is worse in this regard - checked exceptions with all their inconveniences or unchecked with all their mental overhead and constant navigation through foreign code.

Yes, it makes no sense to handle DB call issue, but with Result you don't have to either. So, in this regard, it's a solution at least as good as exceptions. But unlike exceptions, "what you see is what will be executed". No hidden execution paths and relevant issues.

Kotlin "nullable" types, not as good as advertised (non-nullable too, by the way). There are several issues, from non-obvious specifics of the elvis operator to optional chaining which encourages looking deep into the object internals, causing deep coupling.
Not everything gets boxed. Boxed only things which need to be boxed. In normal circumstances there are no several layers of containers, if you need more than one, probably you have a design issue with the code. And definitely this does not hide the true intention of the code. Instead, it preserves valuable information and saves from unnecessary navigation.

For the last few years I'm working in parallel with Kotlin, Java and Rust code. And I did need to look into code of Rust libraries much less time than it happens for Java and Kotlin. And the main reason - exceptions. Another important observation: Option/Result are much more consistent from the point of view of dealing with them, while nulls and exceptions are very different mechanisms and require very different handling.

Collapse
 
pinotattari profile image
Riccardo Bernardini

The lambda syntax in rust is... iffy at best

From your example, I see that it is the same syntax of Ruby (but ruby uses {} instead of ()) and I do not find it so bad. Sure, if the lambda code gets complex, the one-liner can get unreadable, but at least in Ruby you can use the alternative syntax

    users.each do 
        |user|
        #  Do something with user, even quite complex
    end

    File.open('config.txt') do
        |input|
        input.each do 
           |line|
           # Do something with line and input
        end
     end
Enter fullscreen mode Exit fullscreen mode

I really miss my it from kotlin. filter { it.isDigit() } is hard to beat in terms of readability

It seems to me that the difference is just the fact that in kotlin (which I do not know) the "parameter" has the default name it and kotlin uses {} instead of (). In this respect, I prefer the Ruby approach: in short one-liner I can use names like x or c, but for something more complex I can use more meaningful names like (see above) input or line. Also, the Ruby approach allows lambdas with more than one parameter, for example .each when applied to a Hash table allows to give to the lambda both the key and the value.
Finally, I do not know how kotlin behaves with nested lambdas (see the File example above). I guess the inner it "hides" the external one.

About using {} or () I give a (minor) preference to {} since it is more commonly used with code (C inheritance) and maybe the usage of () can introduce some ambiguity.

Collapse
 
martinhaeusler profile image
Martin Häusler

If you have nested lambdas in kotlin, the inner it will "shadow" the outer one. However, this is frowned upon in the kotlin community; for more complex lambdas, we give explicit names to arguments.

So this example:
.filter { it.isDigit() }

... is equivalent to:
.filter { c -> c.isDigit() }

It may seem like a super minor thing, but it comes up so so so often.

In rust, the |c| is only the tip of the iceberg, really. Because lambda arguments can, surprise, have lifetimes. And mutability indicators. Plus, there are three types of lambdas: Fn, FnOnce and FnMut. It gets kinda crazy (for reference, see doc.rust-lang.org/stable/book/ch13...).

Collapse
 
pinotattari profile image
Riccardo Bernardini

Rust is the only language I've ever seen that uses single quotes in a non-pair-wise fashion.

Ada has that too, it is used for so-called attributes. For example, if X is an array you can write

    for  I in X'Range loop
       --  Here I assumes values in the range X'First (which can be different from 0 and 1)
       --  and X'Last
     end loop;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
pinotattari profile image
Riccardo Bernardini

Semicolon!! Gods why.

To be honest, I do not understand why people hate the semicolon so much. It is just a character to type that marks surely the end of the statement. Languages where the semicolon is optional need to "guess" where the statement ends and this introduces questionable syntax (see the continuation mark ... in Matlab/Octave) or some frailty (for example, in Ruby and Python you can have multi-line statements that change meaning if you add few spaces).

No, thank you, give me semicolons. I prefer to type an additional character rather than having a space-sensitive language.

Collapse
 
hwertz profile image
Henry Wertz • Edited

Yeah I used Rust a bit and 100% agree with what you are saying (except I personally don't care about using semicolons personally).

If you're doing something that needs the speed and low levelness of C, Rust protects against quite a few security and "I forgot to cover this corner case in my code" type issues compared to C.

I am surprised at just how popular is has become, given it's a pretty low-level language and I found it rather hard to work with when I just wanted to get something done (I usually use Python myself; I've used Java and like it quite a bit too.)

The couple types of software I work on the most... One "class" of programs I write use web scraping or network APIs, so it spends near 0 time actually doing any computations (I tested with local data and it'd go through about a week of data a second...) and the other class of programs load big data and throw it at CUDA; if you have Python futz with tables, Rust would be faster for that specific part; if you use one of those libs meant for futzing with big tables then it's probably using an optimized C (or possibly Rust!) implementation anyway. And of course the CUDA stuff is just limited by the speed of my video card. In my case, speed of development is massively more important than execution speed since both spend like 99% of the time not actually running my code anyway.

GREAT article! Thanks!

Collapse
 
chts profile image
C.H.

I agree with you on the semicolon and the error handling aspect but the arguing about the keywords IMHO is unnecessary nitpicking

Collapse
 
drumm profile image
Sam J.

Nice to see those first impressions. Most are true, but let’s talk again after 1 month of full time development!

Collapse
 
jonlisec_tvlport profile image
Lisec, Jon

Thanks for posting this Martin, good insights.