1. 61
  1.  

  2. 9

    I’ve been wondering about this exact comparison for a bit, and I think it really comes down to the type of application. I have a SSR webapp written in Kotlin and I have a feeling that I would be less productive in Rust because I’d have to spend more time dealing with borrow checking and compiling the code, rather that tweaking the appearance and business logic.

    This got to be a long post. Addressing the article directly, I’ll go section by section:

    Learning Curve

    No disagreements there, though I’ve been using both on and off for a few years so I’m not the best person to ask at this point.

    Modularity

    This is something that technically exists for Kotlin/Java but just doesn’t have widespread adoption yet with JPMS. With Java modules you can expose just a subset of your public classes and depdencies for others to see when they depend on your module. It does not, however, solve having multiple versions of the same dependency. Not sure if Intellij is using that internally.

    For my own Kotlin project I decided I had enough of Gradle and started using bazel instead. It doesn’t currently support JPMS, but handles modularity to some extent. At work we also broke things up into separate Gradle modules but it’s a bit of a PITA.

    Build System

    No disagreement there, I hate Gradle :) Dynamically typed DSLs that have arbitrary context changes are not my cup of tea. I much prefer bazel, though I haven’t really used Maven.

    Ecosystem

    I don’t really understand this one, JVM has a library for practically everything. Kotlin stdlib also has some goodies that allow me to avoid reaching for libraries like Guava. I’m guessing the plugin doesn’t have many external depdencies because the Intellij plugin lib has most of the code that would be needed for this particular task.

    I am pretty happy with the crate ecosystem as well, there has rare been a time where I couldn’t find a library at all for something, though sometimes it’s version 0.1 and doesn’t do exactly what I need. That’s mostly a consequence of age though.

    Basic Conveniences

    They way I typically code in Kotlin/Java is fairly data centric I say, Kotlin data classes (and coming soon Java records) make immutable data straightforward. Definitely miss traits here though, designing for extensibility upfront it more difficult and can lead to rearranging code or needing intermediate “adapter” classes. Extension methods help make this less painful though.

    Kotlin has sum types in the form of sealed classes (coming to java soon), so it’s possible that the plugin just doesn’t use them for some reason. Sealed classes aren’t as nice to match against since Kotlin doesn’t have real pattern matching (though Java will soonish!). It uses flow typing to tell the compiler “treat this var as type X” in the scope of the match.

    No disagreement with Results, I much prefer them to exceptions.

    Other Kotlin niceties I enjoy are default/named args for methods and constructors, the lambda syntax, extensions methods, shorthand functions/properties, and lambdas with receivers that enable building DSLs.

    Fighting the Borrow Checker

    I mentioned it above, but my feelings about this depend on the application. I don’t want to think about borrowing for a CRUD web server, I want to do business logic. But for the application log processor I’m working on, borrows are important because it let’s me know where and how often I’m copying data around.

    Concurrency

    Rust definitely wins on this front. I haven’t had TOO many issues with it on the JVM. Passing around immutable data to explicitly threadsafe classes makes it easier to deal with. I’ve experienced issues even in well tested libs before (grpc) and have seen people unsure what construct they’re supposed to use when coming from golang.

    Performance

    I can’t really speak to this one. I haven’t really run into issues where the JVM itself was too slow. I have seen peers run into it when I was working in AWS though. I thought it would have been a better fit for C++ or Rust in the first place :)

    Performance Predictability

    No arguments there, GC can be a pain.

    Safety

    Pretty straightforward

    1. 1

      I’m curious if you’ve tried the gradle kotlin dsl, I was verrrry pleasantly surprised with it, it felt like I could actually write some gradle config and get it correct the first time

      1. 1

        I have tried it, it is better though I still had issues. At the time when I tried it the autocompletion was super slow, I had to way ~5 seconds for something to show up. I don’t quite remember but I think there’s still issues with completion for 3rd party plugins because they’re written in Groovy.

    2. 8

      I’ve noticed too that building Rust programs from isolated components/crates with well-defined interfaces is very convenient. I can develop and test each component individually. When they’re not a part of a monolith, I don’t need dependency injection to test them. And then putting components together is as easy as building with Lego.

      Many crates can be small enough to actually be reusable across projects. The larger the library, the more you need to make it flexible and configurable, which adds complexity. But when a crate is trivial, you either reuse it or you don’t, so they can stay simple and focused.

      1. 29

        No offense, but isn’t that how it works with every language? You build libraries, which do their own thing, are tested individually. And then later you connect them all, which is your main app. But that’s easy, since the individual parts are very likely to work.

        1. 28

          On a high level it’s supposed to be like that everywhere, but I see qualitative differences:

          • In C and C++ the culture is to actively avoid having “unnecessary” dependencies. They’re considered a hassle and a liability, so they’re used only if it’s too hard to avoid them. That requires dependencies to be complex or large enough to justify their existence.

            In C splitting a project into separate translation units or even libraries gives almost no isolation: linkable symbols are global, and nothing stops wrong part of the code from pulling in a header it shouldn’t have. In C, proper isolation is something that requires discipline from the programmer, not a tool that keeps lazy programmers in check.

          • npm has the same culture of small modules (and people mock it for left-pad and is-array). Building experience is mostly similar to Cargo, but Rust’s strong type system adds an extra level of assurance. I miss things like docs.rs and guarantees around immutability and borrowing, so in JS I use more defensive coding and I’m more worried that if I change implementation of one module I’m going to break some other module.

          I don’t have much experience with Java, but the few projects I’ve worked on were a single monolith with DI, and used libraries only for 3rd party code, not its own. In PHP it was like that too: monolith on top of a framework + maybe a few libraries for specific things. Microservices are closest to the level of internal splitting Rust projects do.

          1. 8

            In C and C++ the culture is to actively avoid having “unnecessary” dependencies. They’re considered a hassle and a liability, so they’re used only if it’s too hard to avoid them. That requires dependencies to be complex or large enough to justify their existence.

            I see no problem with avoiding “unnecessary” dependencies. Every additional line of code is a potential liability (both technical and legally) in any language, and an additional risk that something will break in the future. It’s just that in C and C++ dealing with dependencies is more painful than “add a line to a dependency file” so you think about it more.

            1. 6

              That’s exactly my point: the C/C++ view is that dependencies are a liability, while Cargo made them work well enough that Rust users see them as a good thing to have.

              an additional risk that something will break in the future

              The alternative view is that dependencies lower the risk of your software breaking in the future, because they’ve been tested by multiple people, on multiple platforms. If they need a fix, someone will patch them before you even realize you needed this (e.g. I don’t know Windows well, so I get better Windows support if I use deps than if I write something myself. Conversely, I send patches to other crates that improve macOS support, and everyone benefits).

              Rust projects choose to split themselves into multiple libraries, because there’s very little downside, and projects benefit from enforced decoupling and easier development on the “leaf” components. In C/C++ you wouldn’t do that, because it seems like a weird thing to do and wasted effort that just makes build scripts more complex. Tooling and culture makes all the difference.

              1. 6

                while Cargo made them work well enough that Rust users see them as a good thing to have.

                I haven’t used Rust but I’m curious; how much of that is Cargo and how much of that is “having a type system that’s not rooted in 1970s ideas”?

                1. 18

                  Pedantic note: don’t blame the 1970s, Hindley published his paper in 1969 and Milner in 1978.

                  1. 5

                    To expand on modularity section of the post, I think this is fundamentally rooted in the module system. Which is, in Rust, mostly orthogonal to type-system (unlike OCaml, modules are not first-class values), but is very much a language concern. Getting rid of a global namespace of symbols is important. The insight about cyclic/non-cyclic dependencies both being useful and the compartmentalization of the two kinds into modules and crates is invaluable.

                    It’s interesting though that the nuts and bolts of the module system are not perfect – it is way to sophisticated for the task it achieves. It’s a shame to spend so much complexity budget on something that could’ve been boring. (I won’t go into the details here, but suffices to say that Rust has two flavors of module system, 2015 and 2018, because the first try was empirically confusing). But this annoyances do not negate the fundamentally right structure.

                    However, I also do think that the theory of depedency hell less library ecosystem would be useless without cargo being there and making it easy to put everything to practice. A lot of thought went into designing Cargo’s UX, and I doubt many people would notice the language-level awesomeness, if not for this tool

                    1. 4

                      It’s a combination of:

                      • ecosystem taking semver seriously (compare to amount of work Linux distros need to put in to keep sem-whatever packages work together)
                      • compiler taking 1.0 back-compat seriously (in contrast, Node.js has semver-major breaking changes regularly, and my old JS projects just don’t run any more)
                      • concept of crates exist in the language (e.g. Rust can have equivalent of -std=cXY set by each dependency individually. C/C++ can’t quite do it, because headers are shared)
                      • not giving up on Windows as a first-class supported platform (in C it’s “not my problem that Windows sucks”. Cargo chose to make it its problem to fix)
                      • zero-effort built-in unit testing. There’s no excuse for not having tests :)

                      Type system also helps, because e.g. thread-safety is described in types, rather than documentation prose. Rust also made some conscious design decisions to favor libraries, e.g. borrow checking is based only on interfaces to avoid exposing implementation details (this wasn’t an easy decision, because it makes getters/setters overly restrictive in Rust).

                      1. 5

                        I’m not sure the Rust compiler, or its surrounding crates ecosystem, take backwards compat “more seriously” than Node does. They just have better tools available to them to detect and fix it. Take this lovely warning, for instance (yes, this is “real” code, not a contrived test case, though I had to check out an old commit to find it because the current version of the program is fixed):

                        warning: cannot borrow `block` as mutable because it is also borrowed as immutable
                        
                            |
                        687 |                 let variable_id = match &block.instructions[place.0 as usize] {
                            |                                          ------------------ immutable borrow occurs here
                        ...
                        691 |                 stack.push(block.push(Instruction::Assign(*variable_id, value)));
                            |                            ^^^^^                          ------------ immutable borrow later used here
                            |                            |
                            |                            mutable borrow occurs here
                            |
                            = note: `#[warn(mutable_borrow_reservation_conflict)]` on by default
                            = warning: this borrowing pattern was not meant to be accepted, and may become a hard error in the future
                            = note: for more information, see issue #59159 <https://github.com/rust-lang/rust/issues/59159>
                        

                        Rust is capitalizing on a lot of technical advantages, here:

                        • It’s able to detect the problem with just static analysis. Comparable problems in Node would probably not be detectable until runtime, which means the problematic code has to actually be reached in order to detect the problem, at which point it might be too late.

                        • Compiler warnings from Rust are precise, rare, and produced around the same time as compiler errors (which you have to pay attention to in order to produce a working program), so Rust has an open mic to communicate deprecation warnings to the developer.

                        • Even if I slept through all of the warnings and got hit with breakage, it would result in my program no longer compiling, not silently changing behaviour or crashing in production.

                        1. 1

                          concept of crates exist in the language (e.g. Rust can have equivalent of -std=cXY set by each dependency individually. C/C++ can’t quite do it, because headers are shared)

                          Can you explain what this means in language-agnostic terms? I don’t know C or C++.

                          1. 3

                            You can use a crate using the “abc” version of the language from a crate using the “def” version of the language. You can’t do this in C/C++ (or really any other language I know of).

                            Rust calls these versions editions, c/c++ calls them… Not really sure but c++11/c++17 and so on.

                            1. 1

                              You can use a crate using the “abc” version of the language from a crate using the “def” version of the language. You can’t do this in C/C++ (or really any other language I know of).

                              Indeed, but you can also use an ABI in C/C++. I’m not excusing the hell that is dependency management in C/C++ projects, but this certainly isn’t specific to Rust. C and C++ can also interact with binary code written in vastly different languages and runtimes, as long as they conform to the ABI of the platform they’re shipping on. Rust cannot do this without work.

                              1. 2

                                I’m not sure why this is a reply to me. It has nothing to do with language versions.

                                It’s also not really true.

                                All of Rust, C, and C++ speak the various “C” abi ’s with about the same level of work. You have to write a language specific description of the calls in the abi, after that they can both seamlessly call into it.

                                In rust this is an extern block, that looks like extern "C" { fn abi_fn(x: i32) -> i32 }, in C this looks like extern int32_t abi_fn(int32_t);.

                          2. 1

                            not giving up on Windows as a first-class supported platform (in C it’s “not my problem that Windows sucks”. Cargo chose to make it its problem to fix)

                            I’ve used Visual Studio for both C and C++ dev on Windows. How does Rust offer better Windows support than C/C++ ? Are you referring to the ease of cross-compilation?

                            1. 6

                              The problem is that Visual Studio doesn’t work on non-Windows systems, and Unix tools like autotools and pkg-config don’t work on Windows (or have quirky ports in mingw/cygwin), so it’s hard to make a project that builds on both. Package management on Windows is a fragmented mess. MSVC has different flags, pragmas and system headers than gcc and clang. C support in MSVC is incomplete and buggy, because Microsoft thinks C (not ++) is not worth supporting. It’s nothing insurmountable, but these are thousands of paper cuts.

                              OTOH: cargo build works on Windows the same way as on any other platform. Unless you’re doing something very system-specific, it just works. And even if you touch something that’s system-specific, chances are there’s already a dependency you can use to abstract that away.

                              Cross-compilation in Rust is not as nice as I’d like. While Rust itself can cross-compile object files and static libraries easily, they need linking and system libraries. Rust uses C toolchain for linking, so it inherits many of C’s cross-compilation pains.

                              1. 1

                                Oh, I see. While I’m not always the biggest fan of Rust’s insistence of doing everything in Rust, I agree that this is a huge win for Rust and Cargo ergonomics. When writing C/C++ in Windows, you are basically writing for a different C runtime, so you need to tailor it accordingly. Rust’s libraries abstract away the differences (though not without great effort, especially if you look at TUI libraries). I really hope other languages continue to explore this space.

                              2. 3

                                C programs written for unix-like operating systems often ignore Windows support entirely. Hence WSL, Cygwin, and so on. Likewise I’m sure few of your Windows C/C++ programs would run on Linux or MacOS unless you exclusively use cross platform libraries and avoid any Windows-specific interfaces.

                                1. 1

                                  Yeah, most definitely. And even the tooling is dramatically different outside of the IDE, with stuff like nmake.

                          3. 2

                            In which way do they work better than in other languages and why?

                            1. 1

                              My point is that every line of code that you write, or that you use, might have bugs or vulnerabilities in them. Dependencies don’t come for free. Any of these dependencies could also have their maintainer just walk away from them, which has bitten me multiple times in the past.

                              Whenever you bring in a dependency, you’re also implicitly depending on all of the dependencies of that thing as well. Tracking dependencies down and being responsible for them is a major issue with the Rust cargo mechanism in that it makes it more difficult for people who have legal liability for code they bring into a project, such as 3rd party contractors.

                        2. 7

                          Yes and no.

                          Heylucas correctly mentions tooling, but it’s also the language.

                          If I’m programming in C or Python and I’m using a library I have to carefully read the documentation to know what I can pass where without breaking things. If it’s java and I’m using a library I have to keep track of a few things like null’s… by carefully reading the documentation. Rust libraries for the most part actually manage to make it difficult to missuse them, so I can be much less careful about reading the documentation.

                          Moreover on the documentation point, rust as a language lends itself to concise and accurate auto generated documentation much more than most languages do, so rust libraries will generally have better documentation despite it being less important. Partially this is tooling, but it is also things like not having SFINAE (C++), not having automatic implementation of interfaces (go), and so on.

                          It is also my belief that the average rust library has less bugs than the average library in the average other language as a result of the language having a greater focus on helping programmers write correct programs (compared to most other languages, which generally focus more on quickly writing programs). As a result the “giant ball of libraries” model is less likely to be a giant buggy mess of others code.

                          1. 5

                            I’m guessing @kornel’s point is more about how easy that is with Rust’s tooling. But I agree, the same can be done for any other language. How easy/convenient it is might depend on the tooling ecosystem.

                            1. 1

                              People have their brains plagged with frameworks to the point that they don’t know how to use their language. Importing a library and test is by simply calling it, is something many developers (cough cough java, c++) already forgot how to do or are not even familiar with to start with.

                              It is a programming language. That dependency injection is not needed, is a simple function of how isolated side effects are. There’s nothing language specific there.

                          2. 3

                            In contrast, Gradle allows free-form project structure, and is configured via a Turing complete language. I feel like I’ve spend more time learning Gradle than learning Rust!

                            I painfully relate to this. I am a rust fan boy but use Kotlin at work. Kotlin is quite nice as well. But gradle behaved really often in surprising ways.

                            Others at my work fell similarly.

                            1. 3

                              Note: I’ve applied java tag as there isn’t kotlin one yet. In my mind, the technologies are sufficiently similar for the purposes of this post, but I don’t know how well this fits tagging guidelines :-)

                              1. 4

                                I think productivity is one case where they are not at all similar. ;)

                              2. 3

                                How do they compare for spotting issues at runtime? With JVM you can see if there is a memory leak by monitoring heap size, GC. Is there similar tooling for Rust (I know there wouldn’t be GC, but something analogous to spot issues)?

                                1. 5

                                  Good question! JVM is much better at this sort of thing, naturally. I miss heap profiling a lot in a Rust, mastiff is just super slow for this. Allocators give you summary stats about the heap though, rust-analyzer uses that with Archemedes Method to weight specific data structures (drop the thing and note total memory diff)

                                  This is a bit compensated for by simplicity of the overall system: no need to monitor GC if there’s no GC :-)

                                  1. 3

                                    You can monitor rust memory usage the same way you can monitor C/C++ memory usage. So tooling exists, but it’s not quite as refined as what exists for go (and what I assume exists for jvm languages - I’m just not familiar with that tooling).

                                  2. 2

                                    The thing I like about rust over any other language (though go is pretty close) is the fact that if it fails to compile (which in a typed language is 60% of the battle, at least getting started) It will tell me what went wrong and exactly where in the documentation it talks about that issue. The time saved there, along with the good documentation and functioning packaging system make rust one of the languages I most hope to be programming in professionally in the near future.