The 1st year anniversary of my first line of Rust code is coming up, and it’s getting for 5 years since I wrote my first line of Scala code. I thought it would be a good idea to summarise my Scala-tinted perspective of The Rust Experience TM, one year on.

Rusty spiral staircase by Jano De Cesare

This is not an objective language vs language comparison. I’ve written this post as part experience dump, part waymark for other Scala devs who are exploring or thinking of exploring Rust.

A bit about me

I’ve written a few Rust libraries/tools as well as Scala ones. For all intents and purposes, I’m a Scala engineer: I get paid to do it and it’s by far my strongest language. I’ve used Rust in a few of my side projects (libraries and smaller utilities).

On the Scala side, I’m the author of enumeratum, which brings flexible enums and value-enums to Scala as a library. I’ve also dabbled in writing macro-based libraries to make things like Free Monads and Tagless Final nicer to use.

On the Rust side, I’ve written frunk, a Rust functional programming toolbelt that is roughly a port of Shapeless with a bit of cats/scalaz mixed in, which does some pretty funky things with the type system that I’ve blogged about (1, 2, 3, 4). I also wrote a Rust port of requestb.in called rusqbin based on Hyper, and a small WIP async client for Microsoft Cognitive services called cogs.

Forewarning

  • I’m biased towards Scala and I’ve mostly gotten used to Scala’s warts. That said, I make an effort to try to be as neutral as possible.
  • When I talk about Rust, I mean Rust stable. This is because I only use Scala stable.
  • Some of the stuff that I write about with regards to Rust might have changed by the time you read this. After all, there is an ongoing ergonomics initiative

Overview

Things I’m happy with

Batteries included

The dev-environment-setup experience with Rust is amazing. The Rust community has striven to make it super easy to get started with Rust and it shows. Literally one shell command will set everything you need up.

  • rustup for managing your Rust toolbelts (different versions/channels of Rust)
  • cargo for managing your build and for publishing to crates.io, which includes, among other things:
  • rustfmt for formatting your code (runs on cargo projects via cargo fmt)
  • rustdoc for generating beautiful documentation websites.
    • This tool supports doc tests with zero additional configuration/setup (runs as part of cargo test)

Coming from Scala, having all of this set up with no fuss right out of the gate is a breath of fresh air and feels like a big win for productivity. I know there are reasons for Scala’s more modular approach, but I think it would be nice if some of this rubbed off on Scala other languages.

Editor/IDE

When I first started with Rust, I used IntelliJ and its Rust plugin, but later switched to Microsoft Studio Code with the Rust plugin, which interfaces very well with Rust Language Server (installable as a rustup toolchain component). It feels very lightweight, and offers all the assistance I need.

Type System

If you lean more towards the functional programming paradigm side of Scala then you’ll probably love the following about Rust’s type system:

  • No inheritance for data types (there is a bottom type but it’s used much more sparingly)
  • No universal equality
  • No nulls
  • Traits are basically Haskell typeclasses
  • Many more primary types (instead of just Int, there are i8, i16, i32, i64, isize, as well as u8, u16 … )

Essentially Rust has a lot of the good things about Scala’s type system. One thing currently missing from Rust is first class support for higher-kinded types (HKT), which, to be honest, I don’t miss too much because:

  1. There are ways to emulate it to an extent
  2. Rust’s ownership/memory model tends to push you towards thinking more granularly about your values/references, something which is perhaps in conflict with the kind of programming typically involving HKT-based abstractions.

If this still sounds unacceptable, just know that you can get quite far in building reuseable abstractions using Rust’s traits + associated types, and BurnSushi’s port of quickcheck is available for writing and enforcing laws.

There are a few interesting things in the pipeline as well:

  1. Higher kinded polymorphism
  2. Pi (value) types

Adding functionality by using Rust’s traits should be familiar territory if you’ve written typeclass-like stuff in Scala. In fact, Rust’s trait system feels a lot more similar to Haskell’s typeclass system than Scala’s, something which has its pros and cons (no scoping of implementations for a given type, for example). I’ve written an intro/guide to Rust’s trait system in another post.

Type inference

Both Rust and Scala have local type inference, and overall, they work in pretty much the same way. In both of them, you need to write the types for your function parameters. In Scala, you can leave the return type off and have the compiler infer it for you, in Rust you can’t (if you leave it off, it is assumed to be (), unit).

Macros

The Rust macro system, while less powerful than Scala’s, is quite useful for keeping your code DRY and importantly, integrates really well with the rest of the language. It is in fact enabled and available out of the box without any additional dependencies/flags.

Compared with Scala’s macros, Rust’s macros feel like a very natural part of the language, and you’ll run into them quite often when reading/using Rust libraries. In Rust code bases, you’ll often see macros declared and used immediately for the purpose of code generation (e.g. deriving trait implementations for a list of numeric types, or for tuples up to N elements), something that Scala users have generally done “out-of-band” by hooking into SBT and using another templating or AST-based tool.

On the other hand, in Scala, the usual refrain is “don’t write macros if you don’t have to”. When I compare the approaches the two languages have taken, I feel that Scala may have been overambitious in terms of giving developers power, thus leading to deprecations of APIs that can’t be maintained due to complexity. Indeed, Scala’s metaprogramming toolkit is going through another reform with the migration to Scalameta.

Because of its simplicity (the macros work based on a series of patterns), Rust’s macro API may feel limiting at first, but if you stick with it, you’ll likely find that you can accomplish more than what you initially thought. For example, the fact that you can build/restructure macro arguments recursively (!) and call the macro again (or even call another macro) is a fairly powerful tool.

Having said that, in addition to the legacy macro system, Rust will soon be getting procedural macros, which are more similar to what Scala devs are used to seeing. You can get a peek of what procedural macros are like by looking at custom derives, which I’ve used to implement derive for LabelledGeneric in Rust.

Compile-time optimisations

I think it’s not news to anyone that Rust is fast and efficient. The home page of the official site says it runs “blazingly fast” and features “zero-cost abstractions”, and the Rust-faithfuls loudly trumpted Rust’s defeat of GCC-C in in k-nucleotide a few months ago. Even if you don’t completely buy into the “faster than C” part, it’s not a big jump to say that Rust performance is in the same ballpark as C, or at least, there is no reason for it not to be (yes, language and implementation are different, compilers make a difference, etc.).

I’m particularly impressed by the Rust compiler’s (though I’m not sure if it’s LLVM?) ability to compile abstractions away so that the operations they enable have zero overhead. As a personal anecdote, when I wrote LabelledGeneric in frunk, I expected there to be some performance difference between using that abstraction for conversions between structs versus writing the conversions by hand (using From). After all, there are non-negligible differences in the Shapeless version of it in Scala land (benchmark code):

1
2
3
4
5
6
7
// JMH benchmark results

[info] Benchmark                               Mode  Cnt     Score     Error  Units
[info] Benchmarks.from24FieldsManual           avgt   30    33.626 ±   1.032  ns/op
[info] Benchmarks.from24FieldsShapeless        avgt   30  4443.018 ± 101.612  ns/op
[info] Benchmarks.from25FieldsManual           avgt   30    33.066 ±   0.650  ns/op
[info] Benchmarks.from25FieldsShapeless        avgt   30  4859.432 ± 104.763  ns/op

To my surprise, Rust manages to compile frunk’s LabelledGeneric-based, non-trivial, multi-step, unoptimised (other than using the stack, no effort was spent) transform between structs into a zero-cost abstraction. That is, using LabelledGeneric for conversion adds zero overhead over writing the transform by hand (benchmark code):

1
2
3
4
5
6
// Cargo benchmark results

test from_24fields_manual           ... bench:         109 ns/iter (+/- 49)
test from_24fields_labelledgeneric  ... bench:         104 ns/iter (+/- 24)
test from_25fields_manual           ... bench:         129 ns/iter (+/- 9)
test from_25fields_labelledgeneric  ... bench:         131 ns/iter (+/- 13)

Note: The Rust vs Scala LabelledGeneric benchmarks are not completely apples-to-apples (the Rust version needs to instantiate new source objects every run because of move semantics), but they illustrate the performance difference between LabelledGeneric-based vs handwritten conversion in the two languages.

Syntax

Overall, the Rust’s syntax is very similar to Scala’s. Sure, there are small adjustments here and there (let and let mut vs var and val, you’ll be using angle brackets instead of square ones, etc), but overall the languages feel very similar because they’re both C-like languages that are heavily inspired by ML.

Scala people will probably rejoice at things like the enum being available (coming soon to Scala via Dotty) as well as partial destructuring (e.g. assuming struct Point { x: i32, y: 32}, you can do let Point { x, .. } = p;).

There are a handful of things that you’ll miss just from muscle memory in the beginning, but are either implemented as libraries or are done slightly differently, such as lazy values (rust-lazy or lazy-static) and methods such as Option’s foreach (try if let Some(x) = myOption { /* use x here */ } instead). Others are just plain missing, such as by-name parameters (not too big of a deal for me), for/do comprehensions, and keyword arguments (these last two hurt).

Oh, in Rust, types and traits are named the same way as in Scala, in CamelCase, but identifiers (bindings and methods) use snake_case, which I still find makes code look longer but isn’t a big problem. You’ll find references that can help if you are unsure and you’ll likely pick it up from reading library code anyways.

As with Swift, I haven’t been able to find conclusive evidence nor credit given to suggest that there was any influence from Scala on Rust …

Interoperability with C

Rust makes working with C as smooth as possible while sticking to its mantra of keeping things safe. For reference take a look at the section in the Rust book that deals with FFI.

1
2
3
4
5
6
7
// Taken from the Rust book
#[link(name = "snappy")]
extern {
    fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}

let x = unsafe { snappy_max_compressed_length(100) };

The syntax might look familiar to those who have played around with Scala.Native.

1
2
3
4
5
6
// Taken from Scala Native homepage
@extern object stdlib {
  def malloc(size: CSize): Ptr[Byte] = extern
}

val ptr = stdlib.malloc(32)

Since calling C-code can be unsafe (wrt memory, thread-safety), Rust requires you to wrap your C-calls in unsafe. If you wish to hide this from your users, you can wrap these calls in another function.

1
2
3
4
5
6
// Taken from the Rust book
pub fn validate_compressed_buffer(src: &[u8]) -> bool {
    unsafe {
        snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0
    }
}

Calling Rust code from C is also very smooth, something that Scala Native has yet to implement.

The current Zeitgeist

The current “feel” of Rust, and its community (or communities, since libraries/frameworks can have their own) is very welcoming and helpful. It’s also very difficult to quantify so I’ll just list some observations:

  • Rust stable is only 2 years old and yet there is an official ergonomics initiative to reduce friction
  • I’ve asked a hand full of questions on StackOverflow and have gotten prompt and helpful answers each time.
  • Rust is the #1 “most loved” language in StackOverflow’s 2017 survey
  • Rust feels very community driven: its got a very lively RFC repo and since I’ve started tinkering in it I’ve seen at least 3 RFCs make it into the language (type macros, custom derives, and ? syntax for Trys).

Things I’ve adjusted to

Semicolons

In Scala, semicolons are optional and almost everything is an expression and therefore return values.

1
2
3
4
5
6
7
8
9
3 // returns 3

val x = 3 // assignment, returns unit

// certain things don't return anything though, such as import
// statements, and blocks

import com.beachape._ // returns nothing
object Hello {} // returns nothing

In Rust, semicolons are non-optional and are of significance. Statements that end with semicolons return () (unit) and those that do not get turned into expressions and thus return a value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// taken from the Rust book

let x = 5u32; // this is a statement

let y = {
    let x_squared = x * x;
    let x_cube = x_squared * x;

    // This expression will be assigned to `y`
    x_cube + x_squared + x
};

let z = {
    // The semicolon suppresses this expression and `()` is assigned to `z`
    2 * x;
};

Ownership model: Stack vs heap

Rust’s memory/ownership model is, to me, its main killer feature; it gives you tighter control over the way your program consumes memory while maintaining memory-safety, all without having to ship a garbage collector with the runtime. You get to decide whether to pass things by value or by reference as well as mutability of bindings (including when pattern matching).

There is also the matter of where things get allocated. In Scala (and perhaps with most JVM-based languages), there are a set of rules that decide whether or not something gets put on the stack or on the heap (and thus incur the future cost of garbage collection). In general, the only time something gets allocated on the stack are primitives that do not escape methods as fields of objects, and references to objects which themselves get allocated on the heap. There might be fun runtime tricks the runtime environment does, like escape analysis, but overall, you don’t get to choose.

In Rust, you can choose to allocate things on the heap by instantiating them inside (or transferring ownership of them to) data structures such as Boxes or Vecs, etc. Or you can choose to work with plain values. You get to pick your abstraction based on the cost you want to pay for the features and guarantees they offer, such as safe multi-thread access (this page is a great reference point). Either way, Rust’s ownership system will, at compile time, make sure that you won’t get data races caused by, for instance, modifying naked values in different threads with no access control.

Scala’s doesn’t give its users the same level of control, so naturally there is some adjustment to be made. However, contrary to the experiences of some others, I didn’t find the ownership stuff too hard to understand and get used to. Having experience with Scala’s rich type system meant that the lifetime annotation stuff was quite easy to come to grips with. Maybe doing C and C++ in Comsci courses in university helped too.

  • Note: If you’re a glass-half-full kind of person, I guess you can say that Rust forces you to take control rather than gives you control. It’s all a matter of perspective …
  • Note 2: If you find yourself doing lots of .clone()s to get the compiler off your back, maybe you’re doing something not quite right.

Mutability

Mutability deserves to be mentioned separately. If you’re coming from years of Scala (or pretty much any other language that stresses immutability and referential transparency as the road to enlightenment), writing your first let mut or &mut self can feel dirty.

It took me a while to get used to the idea, but hey, when in Rome, right? If it helps, remember that Rust is focused on speed and efficiency through (near, or actually) zero-cost abstractions and that, thanks to its strict ownership model, data races due to mutability are not a problem.

Things I wish were different

Async IO

In Scala, most frameworks that deal with any sort of IO have embraced non-blocking IO by utilising some kind of wrapper data type, such as Future[A], Task[A], or IO[A] (usually a Monad), that separates the description of your program from its execution, and identify, by type, the effect of talking with the scary and dirty outside world. This allows you to not block the executing thread when waiting for stuff to happen (such as data to come back) by choosing a suitable execution strategy.

In Rust land, most of the widely-used libraries that I’ve seen, such as the Redis client, and and Hyper (and all the various things built on it, such as Rusoto, Rocket, etc) are all blocking. While this works okay for stuff like single-user utilities, this is suboptimal for applications that are IO heavy and need to serve a large number of concurrent users because your application’s threads can get tied up just waiting for data, leaving it unable to serve other requests. Or, you end up with potentially huge thread pools (à la old school Java Servlet apps..), which seems to go against Rust’s spirit of efficiency.

Having said that I know that advances are being made in this area:

  • Tokio, “tokenised IO”, an async IO framework that exposes a Future-based API is making lots of progress. Looks production-ready.
  • Hyper, the defacto HTTP client server framework, is going to hit 0.11 soon, which will bring with it a Futures-based API based on Tokio. This will likely (I hope) cascade down to any libs based on Hyper.

Also, as of now, it’s painful to transform and return Futures from functions because every transformation causes the concrete type of your object to get chained and tagged with an arbitrary closure type. Since writing the result type is non-optional in Rust, the current solution is to declare your return type as Box<Future<A>>, but it’s less efficient at runtime because boxed trait objects necessitate dynamic dispatch and heap allocation. Hopefully soon “impl Trait” will be released to address this issue (tracking RFC)

Strings

In Rust there are a number of ways to represent Strings. Here are a few:

  • String runtime string value, with its contents allocated on the heap
  • &'a str string with a lifetime
    • &' static str string with static lifetime (baked into your binary)
  • Vec<u8>

While I’ve mostly gotten used to this by now and understand the purpose of having each one, I hope the ergonomics initiative can make this situation better to understand, since strings are so ubiquitous. How? I have no idea..maybe I’m just ranting.

Cross compiling

Obviously, Scala devs are used to compiling once and running the same binaries everywhere thanks to the JVM (mostly :p). While I don’t expect the same for Rust because it compiles to native machine code, I do wish the cross-compilation tools were better out of the box (for example, like it is in Golang).

At the moment, depending on the target platform, cross-compilation for Rust is a bit involved and there are several options:

  1. Adding a target toolchain via Rustup and possibly installing some more packages specifically for your target platform (as in this guide)
  2. Using a pre-built Docker container that holds all the settings/environment variables/installations needed to compile to your target platform (see rust-on-raspberry-docker)
  3. Using the cross, cargo tool that seems like it automates 2.

My use case is building for my Raspberry Pi and I’ve only tried the first 2, but that last one looks to be the winner here and it would be awesome to see something like that included by default as part of rustup or cargo.

Odd headscratchers

Just a few things I still don’t quite get:

Do we actually need ref?

In my opinion, ref is unnecessarily confusing. From what I can tell, it’s mostly used for binding pointers during pattern matching

1
2
3
4
5
match some_int {
  // Why not Some(& s) => ... ???
  Some(ref s) => println!("{}",s),
  None => unreachable!()
}

&mut

When handing out references of something bound with let mut, why do i need to do &mut instead of just & ?

1
2
3
4
5
// This uses mut for no reason other than to prove a point.
fn non_empty(s: &mut String) -> bool { s.len() > 0 }

let mut string = "hello".to_string();
hello(&mut string); // why can't this just be hello(& string) ??

Scoping of lifetimes with braces

I somehow managed to code my way into a deadlock when using RWLock because the lifetime-scoping behaviour of {} braces when used with pattern matching is, in my opinion, non-intuitive. If you’re interested, more about it in this issue.

Gimme

I know these things are in the pipeline but I wish they were in Rust yesterday:

  1. Higher kinded types
  2. “Specialisation”, aka finding the most specific implementation of a traits according to the type of value at the call site. Right now, if you implement a Rust trait for A, then it clashes with every other implementation you write. Specialisation should remedy that (tracking RFC)
  3. A REPL. There’s Rusti but I think Rust is missing a trick by not supplying one out-of-the-box, especially when it’s got such a strong dev-env-setup game.
  4. Some kind of do or for comprehension for working with container types (there are libs out there but built-in would be nice)

Conclusion

That concludes my take on what it’s like to use Rust, from a Scala dev’s perspective, one year on, in 2017. Overall I’m very happy that the me a year ago decided to look into Rust. It’s been a fun and exciting ride: for a while it felt like every few months I was getting new toys that I could immediately use: type macros and custom derives were game changers because they made it ergonomic to write Hlist types by hand, and made Generic/LabelledGeneric practical, respectively.

Overall, I believe there are a lot of things in Rust for Scala engineers to like. The community is friendly and diverse so you can easily find a library that interests you to get involved in (shameless plug: contributions to frunk are always welcome). Or, you can do your own side project and write a small system utility or program a microcontroller; online resources are very easy to find. In any case, with Rust, you really can’t say it’s hard to get started !

Comments