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...
For further actions, you may consider blocking this person and/or reporting abuse
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
That's interesting, I didn't know that! I think it's crazy to determine a method overload based on its return type.
Its generic trait magic actually, not much overloading in rust, the collect method is just implemented for all types which implement the fromiterator trait
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.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.
... 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.
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).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.
I'm questioning if this is a good choice. So far, I don't think so. That may change over time, let's see.
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.
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.
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:
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.
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.
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.
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:
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.
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?
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?
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.
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.
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
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.
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 :)
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.
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.
Java 21 is a significantly different language than Java 8. Basically, Kotlin has no features which would justify switching to it from Java 21.
Data classes (records are incompatible with the bean definition and are always immutable), extension methods, context receivers,
val
instead offinal var
, compiler-checked null safety, better operators for null handling (?:, ?.), multiple compilation targets outside the JVM,if
andtry-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.if as expression
.try-catch
is unnecessary for functional styleit
strips context and is harmfulYes, 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.
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.
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 makesit
a norm for the sake of brevity.I'm also learning Rust and I immediately loved
Result
andOption
enums. The clarity of function interfaces and error handling is amazing. The more you write Rust the more you realize howtry/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
match
ing, how to convert betweenOption
andResult
, how to "rethrow" differentErr
to upper layers, etc.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 rewritingprintf
patterns into AST gave lately 100x speed boost compared to runtime interpolation.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 thanfn
and changing back-and-forth between them is agonizing.Well the rust book in its chapter on error handling (doc.rust-lang.org/book/ch09-00-err...) mentions two ways:
panic!
Result
Which is a valid approach, but I still miss my trusty
try-catch
. And I feel your pain regardingfn
vs.fun
:DRust 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 CNULL
.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.
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.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.
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 syntaxIt 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 likex
orc
, but for something more complex I can use more meaningful names like (see above)input
orline
. Also, the Ruby approach allows lambdas with more than one parameter, for example.each
when applied to aHash
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 innerit
"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.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
andFnMut
. It gets kinda crazy (for reference, see doc.rust-lang.org/stable/book/ch13...).Ada has that too, it is used for so-called attributes. For example, if
X
is an array you can writeTo 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.
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!
I agree with you on the semicolon and the error handling aspect but the arguing about the keywords IMHO is unnecessary nitpicking
Nice to see those first impressions. Most are true, but let’s talk again after 1 month of full time development!
Thanks for posting this Martin, good insights.