This is a follow up to my previous article, in which I explored a few aspects of Kotlin that Rust could learn from. This time, I am going to look at some features that I really enjoy in Rust and which I wish that Kotlin adopted.

Before we begin, I’d like to reiterate that my point is not to start a language war between the two languages, nor am I trying to turn one language into the other. I spent careful time analyzing which features I want to discuss and automatically excluded features that make perfect sense for one language and would be absurd in the other. For example, it would be silly to ask for garbage collection in Rust (since its main proposition is a very tight control on memory allocation) and reciprocally, it would make no sense for Kotlin to adopt a borrow checker, since the fact that Kotlin is garbage collected is one of its main appeals.

The features I covered in my first article and in this one are functionalities which I think could be adopted by either language without jeopardizing their main design philosophies, although since I don’t know the internals of either languages, I might be off on some of these, and I welcome feedback and corrections.

Let’s dig in.

Macros

I have always had a love hate relationship with macros in languages, especially non hygienic ones. At the very least, macros should be fully integrated in the language, which requires two conditions:

  • The compiler needs to be aware of macros (unlike for example, the preprocessor in C and C++).
  • Macros need to have full access to a statically typed AST and be able to safely modify this AST.

Rust macros meet these two requirements and as a result, unlock a set of very interesting capabilities, which I’m pretty sure we’ve only started exploring.

For example, the dbg!() macro:

let a = 2;
let b = 3;
dbg!(a + b);

Will print

[src\main.rs:158] a + b = 5

Note: not just the source file and line number but the full expression that’s being displayed (“a + b”).

Another great example of the power of macros can be seen in the debug_plotter crate, which allows you to plot variables:

fn main() {
    for a in 0..10 {
        let b = (a as f32 / 2.0).sin() * 10.0;
        let c = 5 - (a as i32);

        debug_plotter::plot!(a, b, c; caption = "My Plot");
    }
}
#![crate_type = "lib"] #[test] fn test_foo() {}

Nothing groundbreaking here, but what I want to discuss is the conditional compilation aspect.

Conditional compilation is achieved in Rust by combining attributes and macros with cfg, which is available as both an attribute and a macro.

The macro version allows you to compile conditionally a statement or an expression:

#[cfg(target_os = "macos")]
fn macos_only() {}

In the code above, the function macos_only() will only be compiled if the operating system is macOS.

The macro version of cfg() allows you to add more logic to the condition:

let machine_kind = if cfg!(unix) {
    "unix"
} else { … }

At the risk of repeating myself: the above code is a macro, which means it’s evaluated at compile time. Any part of the condition that is not meant will be completely ignored by the compiler.

You might rightfully wonder if such a feature is necessary in Kotlin, and I asked myself the same question.

Rust compiles to native executables, on multiple operating systems, which makes this kind of conditional compilation pretty much a requirement if you want to publish artifacts on multiple targets. Kotlin doesn’t have this problem since it produces OS neutral executables that are run on the JVM.

Even though Java and Kotlin developers have learned to do without a preprocessor since the C preprocessor left such a bad impression on pretty much everyone who has used it, there have been situations in my career where being able to have conditional compilation that includes or excludes source files, or even just statements, expressions, or functions, would have come in handy.

Regardless of where you stand in this debate, I have to say I really enjoy how two very different features in the Rust ecosystem, macros and attributes, are able to work together to produce such a useful and versatile feature.

Extension traits

Extension traits allow you to make a structure conform to a trait “after the fact”, even if you don’t own either of these. This last point bears repeating: it doesn’t matter if the structure or the trait belong to libraries that you didn’t write. You will still be able to make that structure conform to that trait.

For example, if we want to implement a last_digit() function on the type u8:

trait LastDigit {
    fn last_digit(&self) -> u8;
}

impl LastDigit for u8 {
    fn last_digit(&self) -> u8 {
        self % 10
    }
}

fn main() {
    println!("Last digit for 123: {}", 123.last_digit());
    
}

I might be biased about this feature because unless I am mistaken, I was the first person to suggest a similar functionality for Kotlin back in 2016 (link to the discussion).

First of all, I find the Rust syntax elegant and minimalistic (even better than Haskell’s and arguably, better than the one I proposed for Kotlin). Second, being able to extend traits this way unlocks a lot of extensibility and power in how you can model problems, but I’m not going to dive too deep into this topic since it would take too long (look up “type classes” to get a sense of what you can achieve).

This approach also allows Rust to mimic Kotlin’s extension functions while providing a more general mechanism to extend not just functions but types as well, at the expense of a slightly more verbose syntax.

In a nutshell, you have the following matrix:

KotlinRust
Extension functionfun Type.function() {...}Extension trait
Extension traitN/AExtension trait

cargo

This probably comes off as a surprise since with Gradle, Kotlin has a very strong build and package manager. The two tools certainly have the same functional surface area, allowing to build complex projects while also managing library downloading and dependency resolution.

The reason why I think cargo is a superior alternative to Gradle is because of its clean separation between the declarative syntax and its imperative side. In a nutshell, standard, common build directives are specified in the declarative cargo.toml file while ad hoc, more programmatic build steps are written directly in Rust in a file called build.rs, using Rust code calling into a fairly lightweight build API.

In contrast, Gradle is a mess. First because it started being specified in Groovy and it now supports Kotlin as the build language (and this transition is still ongoing, years after it started), but also because the documentation of both of those is still incredibly bad

By “bad”, I don’t mean “lacking”: there is a lot of documentation, it’s just… bad, overwhelming, most of it outdated, or deprecated, etc…, requiring hundreds of lines of copy/paste from StackOverflow as soon as you need something out of the beaten path. The plug-in system is very loosely defined and basically lets all plug-ins access whatever they feel like inside Gradle’s internal structures.

Obviously, I am pretty opinionated on this topic since I created a build tool inspired by Gradle but using more modern approaches to syntax and plug-in resolution (it’s called Kobalt), but independently of this, I think cargo manages to strike a very fine balance between a flexible build+dependency manager tool that covers all the default configuration adequately without being overwhelmingly complex as soon as your project grows.

u8, u16, …

In Rust, number types are pretty straightforward: u8 is an 8 bit unsigned integer, i16 is a 16 bit signed integer, f32 is a 32 bit float, etc…

This is such a breath of fresh air to me. Until I started using these types, I had never completely identified how uncomfortable I had always been with the way C, C++, Java, etc… define these types. Whenever I needed a number, I would use int or Long as default. In C, I sometimes went as far as long long without really understanding the implications.

Rust forces me to pay very close attention to all these types and then, the compiler will relentlessly keep me honest whenever I try to perform casts that can lead to bugs. I really think that all modern languages should follow this convention.

Compiler error messages

Not to say that Kotlin’s error messages are bad, but Rust certainly set a new standard here, in multiple dimensions.

TestNG is a project I started around 2004 with the only intent to mix things up. I wanted to show the Java world that we could do better than JUnit. I had no intentions for anyone to like and adopt TestNG: it was a project lab. An experiment. All I wanted to do was to show that we could do better. I was genuinely hoping that the JUnit team (or whatever was left of it) would take a look at TestNG and think “Wow, never thought of that! We can incorporate these ideas in JUnit and make it even better!”.

This is my goal with these couple of posts. I would be ecstatic if these two very, very different worlds (the Rust and Kotlin communities) would pause for a second from their breakneck development pace, take a quick look at each other, even though they really had no interest in doing so, and realize “well… that’s interesting… I wonder if we could do this?”.

Discussions on reddit: