Rust for the Seasoned Scala Developer

Updated: removed lack of extension methods as they can be implemented using traits, added language editions

Intro

Scala has been my language of choice (both for work and personal projects) for at least 10 years now. No other language has even made me consider switching:

  • Kotlin is basically a lesser clone of Scala with a weaker type system
  • Java and C# have both improved greatly, but are still lacking important features (like type classes/implicit parameters)
  • Go is a no-go because of the badly designed type system
  • Swift has a pretty nice type system, but it's not better than Scala in any way really, has worse memory management (basically implicit reference counting) and the portability is bad
  • TypeScript also has a pretty nice type system, but again doesn't provide any benefits for general application development, and if I want to target the browser I'll rather use Scala.js

That pretty much just leaves Rust, which as an old C++ developer, always has intrigued me as it provides solutions for some of the performance issues of the JVM. I've followed the development of Rust for a couple of years, but only recently have I really used it extensively. To put it mildly, I'm really impressed by the Rust language design. In this post I'll cover some of the up- and downsides of Rust seen from an experienced Scala developer's perspective (some of them are also relevant for Java, C# and Kotlin developers).

Basic Rust Language Features

Rust has all the basic language features from Scala I would expect from any modern programming language:

Algebraic Data Types

Rust ADT's (using enum and struct) are very similar to what you find in ML or Haskell. Scala's sealed types and case classes are a bit more powerful (you can for example add additional type parameters on an enum variant), but Rust ADT's are perfectly adequate for most use cases.

Pattern Matching

Rust's pattern matching is similar to what you find in Scala. One difference is that you can't define your own pattern extractors in Rust. I don't think I've ever used this feature in Scala, so I can't say I miss it, and not having it avoids another form of magic implicitness.

Just like in Scala you can use pattern extraction in variable initialization (let/val). And there is if let and while let which corresponds for loop patterns in Scala.

Closures

Rust has support for closures which are function objects that capture values from the local environment. Because of the ownership system (which I'll elaborate further on below), they come in three different forms (FnOnce, Fn and FnMut) depending on how the capturing is performed. So, they are a bit trickier to use than in Scala, but it's not a big inconvenience.

Module System

Rust has a pretty basic module system for encapsulation and exporting, similar to Java packages or Haskell modules. The modules are not first class so they are not nearly as powerful as Scala objects or ML modules, but for most use cases it's good enough and traits can be used to sort of fill in the missing functionality.

There's also crates which contains one or more modules packaged together similar to a Jar-file or a Maven package.

Generics

As you would expect, Rust has support for adding type parameters to functions and data types. Ad hoc polymorphism is supported by specifying trait bounds on the type parameters. This is similar to Scala context bounds and implicit parameters, except there is no accessible, additional parameter value representing the trait implementation (see traits below).

Recently, support for const generics has been added which is required for some use cases like fixed size arrays.

Traits

Rust's trait system is one of the highlights of the language (the other being the borrow checker). It's very well designed and really intuitive to use.

There are many traits included in the standard library, many of which are treated specially in the compiler:

  • Send, for types that can be safely sent between threads
  • Sync, for types that can be safely shared between threads
  • Deref, automatic de-referencing using the dot operator, very useful for smart pointer types for example
  • Drop, for deterministic freeing of resources
  • Copy, for types that can be cheaply, implicitly copied
  • Clone, for types that can be copied, but always explicitly and not necessarily cheaply
  • Eq, for equality comparison using the ==-operator
  • Hash, for hash value calculation
  • Add, Sub etc. for numerical types
  • etc, etc.

I find it amazing how well they've managed to use traits for specification of data type properties, and how the compiler and borrow checker then uses these traits to reason about the types. In many other languages this would be a bunch of new language keywords, but encoding it as traits makes everything much simpler, more coherent and compositional.

Some traits are automatically implemented for a type unless you opt out of them, others you can implement automatically with the derive attribute, and some you just have to implement manually. You can also write derive macros for your own traits (similar to what you can do in Scala 3). All in all, it's a very convenient and flexible system for implementing traits for your data types.

A trait implementation is always coupled with a value of the type it's implemented for, there's not a separately accessible value like with Scala implicit parameters. In a way it's similar to trait inheritance in Scala as you have a self argument when implementing trait methods (corresponding to this in Scala), but also similar to implicits in the sense that you can add implementations of new traits for existing data types. So, it combines the two different ways of ad hoc type polymorphism in Scala into a single construct, vastly simplifying the type system.

Unlike Scala's implicits, in Rust you can only define a trait implementation for a data type in same crate as the trait or the data type. This enforces coherence and avoids multiple conflicting implementations of the trait. Although this limits the power of traits compared to implicit parameters, I think in most cases this limitation is fine, and to work around it you can always define a new type (a tuple struct) wrapping the type you want to add a trait implementation for. As most things in Rust, this wrapping is a zero cost abstraction (sort of like value or opaque types in Scala).

Language Features That are Missing in Rust

Rust is missing some language features from Scala:

For Comprehensions

For comprehensions is one the major Scala features missing in Rust. For functional programming it's really nice to have, but maybe I won't miss it that much in Rust as it has a more imperative style. Only time will tell...

Higher Kinded Types

HKT's are not supported in Rust, but associated type constructors which has similar expressiveness may be added at some point (for a description see: part 1, part 2, part 3 and part 4). Unless you do pure functional programming using constructs from category theory I don't think you will miss HKT's much.

Subtyping and Inheritance

Rust doesn't support data type subtyping/inheritance. Personally I don't miss it at all and not having it simplifies the language a lot and avoids the problems associated with it (diamonds, fragile base classes, unexpected type inference etc.). Traits have a form of subtyping which covers most use cases anyway. And since you have zero overhead abstractions (see below), composition is basically as cheap as data type inheritance. 

There is also subtyping relationships for lifetimes.

Other Type System Features

There are some other type system features in Scala that are missing in Rust, for example dependent types, existential types, union and intersection types (although you can have intersecting trait bounds). Honestly, I really don't think I will miss them much as they are seldom necessary.

Things I Like In Rust

Zero Cost Abstractions

Rust's zero cost abstractions is a big advantage in any application where performance and low memory overhead is important. In Scala you have to go through hoops (using value classes, opaque types, specialization etc.) to avoid heap allocation and boxing, which can destroy performance in tight loops. In Rust everything is monomorphized and thus optimized by default, you have to explicitly opt out of it with dyn trait for example. And there are no implicit object header overheads for locks, reflection, hash codes etc. unless you explicitly ask for it.

Explicitness

Compared to Scala, Rust code contains a fair amounts of explicitness in regard to type conversions, borrowing, self/this etc. In the beginning this seems a bit overly pedantic and boilerplatey, but after a while you really come to appreciate how it makes Rust code easier to read and understand without an IDE. In Scala I really need the help of an IDE to follow what's happening in the code. There are some implicit things in Rust like the Deref and Copy traits, trait dispatch etc., but you quickly learn about those few exceptions. 

Type inference only works locally in functions in Rust, not for return types, fields etc. as in Scala. I think this is a good thing as it requires you to type annotate public interfaces.

There is also lifetime elision which implicitly creates and applies lifetime parameters, but it's not a big hindrance in readability as you quickly learn the elision rules.

Data Type Flexibility

In Rust you don't specify at the definition site if a data type has value or reference semantics, or if it's immutable or mutable. You can use however you like as long as you follow the type system rules, which is great.

Ownership, Lifetimes etc.

Coming from a garbage collected language the borrow checker can be a bit of an annoyance when you start learning Rust, but the more you use it you actually come appreciate it and see the advantages of it. It enforces a very natural way of thinking about data ownership and lifetimes, and it enables many useful patterns (like deterministic resource management).

Low Level Memory Control

Related to the previous section, is the low level control over memory management in Rust. The combination of smart pointers (Box, Rc, Arc etc.) and interior mutability (any type built on UnsafeCell) is powerful enough implement any data structure you can write in a garbage collected language (see my previous blog posts for more info). Sure, there will be more boilerplate in some cases, but in return you get full control over things like thread safety, object placement, lifetimes, runtime performance and memory overhead. If you're an old school C++ programmer like me it's awesome to get back to that kind of low level control/optimization after all the years of using VM's!

Memory Safety

Coming from Scala there are basically two big benefits of Rust's memory safety: no null pointers and no data races when writing code using threads, locks etc. On the other hand, the other aspects of Rust's memory safety are huge benefits when coming from C and C++.

No Runtime System

Rust is compiles to native code and doesn't have a runtime system. This means you can use it for anything from the most simple embedded system, the Linux kernel and drivers, up to cloud server systems. Sure, a runtime system can be nice to have sometimes when you need monitoring and introspection, but otherwise I don't miss the JVM at all.

Error Handling

Error handling in Rust is refreshingly straightforward and safe, you just use the Result type in combination with the ?-operator. In Scala on the other hand you have Java exceptions, which are not recommended to be used and is a common source of unexpected application crashes, and you have Either and Try, but many libraries don't use them and instead define their own result types. It's just a mess.

There is also panics in Rust, but that should only be used for unexpected errors which shouldn't be handled. Sort of like RuntimeException in Java.

Language Editions

Rust has support for language editions which can contain backwards incompatible language changes. In your crate definition you specify which edition you are using, but you can still depend on crates which use a different edition. I think this is a great idea for keeping the language clean and being able to correct old design mistakes.

Scala 3 contains some breaking language changes and the compiler team has put a lot of work into making the transition from Scala 2 as smooth as possible. If Scala would have had support for language editions I think the amount of work could have been reduced substantially.

Tooling

Compiler

Rust really has some awesome tooling that's worth mentioning. The Rust compiler itself is great and has really helpful error messages which often suggests the correct solution for the problem (really useful when wrestling the borrow checker). I have to say it's way better than the Scala compiler when it comes to error messages, but maybe Scala 3 will improve upon that.

Package Manager and Build System

Cargo is an exceptionally good build, package and dependency tool that's very simple to use compared to SBT, and it just works flawlessly. I've never been a big fan of SBT, it always felt over-engineered for the simple use case. Cargo feels just powerful enough for the task.

IDE Support

Regarding IDE support, I exclusively use IntelliJ for Scala and it's great. For Rust I use the rust-analyzer plugin in VS Code, and so far I haven't missed any features compared to IntelliJ Scala. Basic functionality is there, it shows type hints and it has some handy quick fix actions. It's probably missing some of of the refactorings you can find in the IntelliJ Scala plugin, but I seldom use those anyway. I haven't tried the IntelliJ Rust plugin, but it's supposed to be on par with rust-analyzer.

Debugging

Debugging Rust code on Windows works fine in VS Code, in some ways it's a better experience than debugging Scala code because the data inspector is way less cluttered with compiler generated variables and nested objects.

Complexity

Rust and Scala are both necessarily complex languages, but for different reasons. Scala's complexity comes from the requirement of Java interoperability and the advanced type system with subtyping, Rust's complexity comes mainly from the fact that it doesn't rely on a garbage collector in combination with strict performance and memory safety requirements.

Learning Curve

So, which language is easiest to learn and become productive with? Well, if you have experience with modern C++, I think Rust is the easier one to learn. However, if you come from an OO language like Java, Kotlin, C#, TypeScript, Swift etc. you will get productive in Scala faster as you can write basically similar code, however it will take some time to learn the purely functional style and the more advanced parts of the type system. Coming from one of these languages to Rust on the other hand, you will definitely struggle with the borrow checker for a while, and you will find many new things to learn in the standard library (like all the traits and smart pointers) before you can even start to build a bigger application in Rust.

Both languages are fantastically interesting, and you will learn a lot about programming in general by becoming proficient in either of them.

Writing and Reading Code

In general I think Scala code is easier and faster to write than Rust code, it feels more like a scripting language with more type inference and automatic memory management. The REPL and IDE worksheets are great tools for quick Scala prototyping.

On the other hand, due to the increased explicitness in Rust code I think it's a bit easier to read and understand than Scala code, even though there is a quite a bit more visual noise (especially compared to the Scala 3 optional braces syntax). 

Code Quality and Maintainability

For a large applications, code readability and maintainability is much more important than being able to quickly write code, and I think Rust can have an advantage here (although I'm not experienced enough to say for sure). Especially with an inexperienced team I think Rust can be a better choice as it gives you less options for writing non-idiomatic code, and as long as you don't use unsafe Rust the compiler gives you a really nice safety net.

Scala is also mostly safe, but it requires some experience and discipline to write code that is concurrency safe and doesn't throw runtime exceptions in production. For an inexperienced developer Scala just enables a too many bad options, while the Rust compiler basically forces you to write idiomatic code.

Rust or Scala?

So, what type of applications would I choose Rust for and which ones do I still think Scala is a better choice? Well, for some types of applications the choice is easy:

  • For low level applications like kernels, drivers, embedded software etc. Rust is the only choice
  • For a command line tool I would choose Rust. Although you can build native applications in Scala, Rust is just better for building efficient for small, efficient applications.
  • For native game development, Rust is way better
  • For purely functional applications running in a web server cluster for example Scala is way better: the Scala type system has better support for functional programming, the JVM has great garbage collectors and monitoring, and there are many great libraries/frameworks available like ZIO, Cats etc.

For other types of applications it's a harder choice:

  • Both languages are suitable for a browser application (using WASM for Rust and Scala.js for Scala). However, if you have a browser application that doesn't have high performance requirements and is communicating with a server written in Scala, Scala.js is a better choice as you can share code between client and server. For browser games or anything requiring high performance, Rust is probably better.
  • For a GUI desktop application it depends on if performance is critically important, if so, I would lean towards Rust, otherwise I would prefer Scala as the JRE has great platform independent GUI libraries built-in. Last time I checked Rust was a bit lacking in platform independent GUI libraries. 
  • For mobile apps, I'm not so well versed in that area. I know Scala applications be run as normal Java applications on Android, and I think you can compile natively to iOS as well using Scala native or Graal. Rust applications can be compiled natively to both platforms (I think). Again, if performance is critical Rust is probably a better choice, otherwise both languages should be fine.

Final Words

I'm very pleasantly surprised by Rust, it's amazing how it can even compete with a language like Scala in terms of expressiveness and type safety considering that it's so close to the metal. It's a great combines of both worlds. I'm still quite a bit more comfortable writing Scala code than Rust code (it would be surprising otherwise), but that difference will decrease over time as I continue to use/learn Rust.

If I were to choose just one language to use for all my code, it would be Rust as it's more generally applicable for all application types. Scala is however, unsurprisingly, a much better choice for pure functional programming, and I don't see Rust ever reaching the same level of practicality there. So, for the foreseeable future I will continue to use both languages :)

Comments

  1. Nice article! I think learning both Scala and Rust can be very useful for programmers. These languages are using important designs.
    And by the way, I'd suggest you to try IntelliJ Rust plugin. That's fantastic!
    Happy coding!

    ReplyDelete
    Replies
    1. Thanks!

      Yes, as I write, both languages have really great design and teach you a lot about programming in general. Scala's type system (based on the DOT calculus) is just amazingly powerful, and Rust's combination of system level programming and high level abstractions is so well put together!

      Delete
    2. And yes, I will try the IntelliJ Rust plugin. The only downside is that for debugging you need to buy a CLion license, right?

      Delete
    3. I think in new version of the plugin, it can be used to debug in other commercial IDEs also. (Like IntelliJ ultimate and PyCharm etc.)
      But that's not available in open source versions.

      Delete
  2. Thank you for this nice article. My two cents:
    Rust has also nice FFI support and on crates.io one can find several helping libraries for this purpose. Rust is also easy to compile to shared libraries, so combining Rust and Scala code in the same project is also an interesting option.
    It can be a bit tricky sometimes because it requires (depending on the API) to tell Rust to "forget" about variables that are going to be managed "manually" (i.e. memory freed by hand). However, this can provides the best of both worlds :D

    ReplyDelete
  3. If you ever used pattern matching to extract groups in a regex expression, you have used the ability to define your own extractors. I believe there are a few other places in the standard library that also makes use of this feature, let alone the myriad of third party libraries. Many features of Scala exist to hide the implementation, so, for example, you can think of List as a sealed trait composed of the case object Nil and the immutable case class ::, whereas the actual implementation is rather different than that for performance reasons (or was -- I haven't looked at its current implementation on 2.13 or 3 RC).

    ReplyDelete
    Replies
    1. Yes, I know it's used for regex patterns, which I almost never use myself. I'm not saying it's a useless feature, just that I won't miss it basically at all.

      I've come to the conclusion that all a good language really needs is ADT's, type classes (or similar), closures and first class functions, and optional mutability and arrays. That covers 95% of all the code you write, other language features are mostly icing on the cake. Although I miss things like Scala's dependent types and other type level features when modelling more complex structures in Rust.

      Delete
  4. Good article! I'm motivated for trying Rust.

    ReplyDelete
    Replies
    1. Thanks! I think it's worth trying out Rust, even if you don't end up using it, it's a fun learning experience.

      Delete
  5. Great writeup Jesper. As a Scala guy I was just on a verge of giving Rust a try :)

    ReplyDelete
    Replies
    1. Thanks! Give a try, you might like it :)

      Delete

Post a Comment

Popular posts from this blog

Data Modelling in Rust

Data Modelling in Rust Continued