Loving Haskell Through Rust

Originally written: March 4th, 2021

I've been learning the Rust programming language in order to achieve another goal laid out in my goals for 2021. I'm still at baby steps, barely making a simple snake game, but I've been incredibly pleasantly surprised so far. I thought Rust would be similar to C or C++, especially given its syntax. However, I've realized that while Rust is mostly an imperative language, it borrows a lot of ideas from functional programming. Ironically, learning Rust made me realize why I did (and do) love Haskell. Here are some features of Rust that I love -- that reminded me of why I love Haskell. (Although most of it probably also applies to other functional languages, like the ML family.)

Strong compile-time error checking. A saying goes in Haskell, "if it compiles, it probably runs". That doesn't necessarily mean that the code is necessarily right, but that it's a lot less likely to have runtime errors. As much as the joke goes, it is actually nice to write Haskell because the strong types will force you to really check your code. And same goes for Rust. It's a language that strives for compile-time error checking. As somebody working in formal methods, it's hard for me to not love this. Type checking aside, you can see this philosophy baked into the language. Catching array out of bounds errors at compile time. Raising errors when never "collecting" a lazy iterator (something that Haskell actually doesn't do). Non-exhaustive pattern matching not being a warning, but an error.

Traits. Typeclasses are probably one of Haskeller's favorite things about the language. It's a way of enforcing compile-time error checking, and besides, it's nice to organize ideas in a hierarchial, inheritance way. Object-oriented languages also have interfaces, but I've always found them to be rather bulky, and somehow not as closely tied with the language as typeclasses. Well, Rust reminded me of this love of typeclasses with its traits. Traits are virtually same as typeclasses; even the way you implement them, "inherit" them, assign them to new types, etc.

Enums. Talking about typeclasses naturally leads to the discussion on Abstract Data Types. This is one of the greatest features I miss while coding in other languages, and I'm glad that Rust has implemented them with Enums. I actually think that Rust has it a little bit nicer, by being able to organize the functions associated with a type in an impl clause. Even if you only write pure, static functions, I think it's a nicer way of organizing; I always thought it was a little bit disorganized to have a Haskell ADT and associated functions flying around everywhere. I don't know much about Ocaml, but I know they have modules, and it's nice that Rust is liberally using the best ideas possible.

Immutability. Rust variables are, by default, immutable. Although it allows mutability, and functions are not necessarily all pure, I appreciate immutability in the same way I appreciate immutability in Haskell: it's likely to make your code a lot less buggy.

Difference: side-effects and imperative language features. The general consensus in the Haskell community is that it might be harder to develop in Haskell (compared to other languages), but that maintaining projects becomes a lot easier, thanks to the purity of its functions. However, Rust can have all the side-effects you want, and if you make variables mutable, you can essentially write C code in Rust (after all, you can write FORTRAN in any language). This, in part, comes from Rust being a mostly imperative language, despite a lot of its design features coming from functional languages; it doesn't necessarily have the same targets as functional languages do. For instance, it's very difficult to create an operating system in a side-effect-free language. This also gives rise a whole host of bugs -- kernel hackers can easily make a tiny deadlock bug and make their entire kernel unbootable.

That being said, imperative language features are nice in their own regard. They may have more bugs, but they're also easier to debug, thanks to print statements (Haskell's Debug.Trace is nice, but being able to step through code is still a lot easier.). Computer architectures are laid out in von Neumann architectures, so imperative languages also model the actual registers and computer more closely. Most algorithms are developed with imperative languages in mind; it's always possible to make these algorithms functional, but that often can take non-trivial effort.

In the end, Rust and Haskell are different languages, after all. Overall, I'm just glad that I got to see Rust -- because it does borrow so many nice things from Haskell.