Rust for the Seasoned Scala Developer
Updated: removed lack of extension methods as they can be implemented using traits, added language editions
- 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
Algebraic Data Types
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.
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.
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.
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.
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.
Language Features That are Missing in Rust
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
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
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.
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
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!
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 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.
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.
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.
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 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.
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.
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.
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 :)