Rust vs. Haskell: A performance comparison

mangelosanto

Matt Angelosanto

Posted on October 25, 2023

Rust vs. Haskell: A performance comparison

Written by Frank Joseph✏️

The Haskell and Rust programming languages allow developers to build powerful web and system software. And while they share some similarities, each boasts distinct features that make it fit for unique use cases.

In this article, we’ll offer a comparative analysis of Haskell and Rust, focusing on essential performance parameters such as memory safety, concurrency, type safety, and variable immutability. Jump ahead:

An overview of Rust

Rust Logo Rust is a multiparadigm, statically typed system programming language developed by Graydon Hoare at Mozilla. Although it shares some similarities with the C and C++ programming languages and offers the speed rate of these languages, Rust was designed to address areas where C-based languages had shortcomings.

For example, the Rust compiler enforces data thread safety, which was not offered in C, and the use of parsed data is fast and efficient with Rust due to its fast JSON parser, Serde.

Rust also enforces memory safety without a garbage collector, which is an important aspect of memory management involving the release or reclamation of allocated memories that are no longer being referenced in a program. A garbage collector negatively impacts the performance of a program as it regularly runs to check for object references and frees up memory, activities which can consume resources and sometimes require the program to pause. Languages like Java, Haskell, and Lisp use garbage collectors.

Additionally, Rust frees or reclaims memory through its system of ownership and borrowing. Read more about that in this guide to Rust ownership. Rust borrowing means that multiple variables can access a piece of memory. Variables in Rust can be borrowed mutably or immutably. The code snippets below illustrate how a variable can access a piece of memory through immutable borrowing:

fn main()
{
  let y = String::from(LogRocket); // y owns LogRocket
  let x  = &y; // x references y, borrowed LogRocket
  println!({}, y);
  println!({}, x);
}
Enter fullscreen mode Exit fullscreen mode

In the code example above, we created two variables: x and y. In x, we assigned the string LogRocket, and in y, we assigned the immutable reference of x. The variable y has borrowed the value of x. Because y is an immutable reference, it can only read the value of x — it cannot modify it because it doesn't directly point to the value; instead, it points to the variable x. Therefore, the value of x is borrowed by y as immutable.

In mutable borrowing, a variable can borrow a value, read it, and modify it. This is achieved by the use of the mut keyword. The code snippet below illustrates how a variable can create a piece of memory through mutability:

fn main()
{
  let mut y = 50 // create a mutable variable
  let x  = &mut y  // create a mutable  borrow
   x = 5  // modify the variable 
}
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, we created a mutable variable y, and initialized it with the value 50. We created a mutable borrow x by using &mut y, which allows us to modify y through x. The combination of this system of ownership and borrowing helps the Rust compiler manage memory-related bugs at compile time.

Key Rust features include:

  • Memory safety
  • Developers using Rust can implement concurrency
  • Interoperability with C
  • Zero-cost abstraction
  • Minimal runtime

An overview of Haskell

Haskell Logo Haskell is a purely functional, multipurpose, and statically typed programming language, popular for its lazy valuation, type inference, and expressive syntax.

Unlike traditional imperative programming languages, Haskell offers a unique paradigm that allows developers to write concise and meaningful code. The language is used in academia due to its mathematical and scientific computing capabilities.

The Haskell programming language was designed by a committee whose purpose was to consolidate existing functional languages into a single language for the purpose of research. This has positioned Haskell as an advanced functional programming language suitable for rapidly developing robust, concise, and efficient software products:

Key features of Haskell include:

  • Purely functional
  • Supports lazy evaluation
  • Supports type inference
  • Supports static typing
  • Supports pattern matching

Rust vs. Haskell: Performance features

Memory safety

Rust’s memory safety is intended to provide a high-level memory performance and mitigate against common, memory-related bugs like null pointer, dereferencing, and data races. This is achieved through the ownership and borrowing system.

Using Rust’s ownership and borrowing, memory-related bugs including buffer overflow, use-after-free errors, null-pointers, and data races are managed.

Meanwhile, Haskell uses the garbage collection mechanism to manage memory and lacks the low-level memory management feature seen in Rust and other low-level languages like C. While garbage collection relieves the developer of the stress of manual memory management, it can sometimes contribute to occasional breaks or pauses in real-time systems.

Concurrency

Concurrency improves the efficiency of a system as multiple tasks are executed or processed simultaneously. Both Rust and Haskell handle concurrency using different approaches. Haskell uses software transactional memory (STM), which works by isolating write and read functionalities to shared memory locations in a transaction.

In contrast, Rust uses different concurrency abstractions such as multiple threads (executing a process concurrently across CPUs), sharing states concurrently (sharing data across multiple threads), message passing (using channels to send messages between threads), data race prevention, and more.

The code snippet below illustrates how Rust implements multiple threads:

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("Frank's thread number {} ", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("Imane's thread number {} ", i);
        thread::sleep(Duration::from_millis(1));
    }
}
Enter fullscreen mode Exit fullscreen mode

The output of this code should look like the image below: The Output For How Rust Implements Multiple Threads The code snippet below illustrates how Rust implements sharing states concurrently:

use std::sync::Mutex;

fn main() {
  let m = Mutex::new(10);
  {
      let mut num = m.lock().unwrap();
      *num = 8;
  }

  println!("m = {:?}", m);
}
Enter fullscreen mode Exit fullscreen mode

The output of this code should look like this: An Outcome For How Rust Implements Sharing States Concurrently The code snippet below illustrates how Rust implements message passing:

use std::sync::mpsc;
use std::thread;
fn main() {
  let (sender, receiver) = mpsc::channel();
  for i in 0..3 {
      let sender_clone = sender.clone();
      thread::spawn(move || {
          sender_clone.send(i).unwrap();
      });
  }

  let mut received_data = vec![];
  for _ in 0..3 {
      received_data.push(receiver.recv().unwrap());
  }

  println!("Received data: {:?}", received_data);
}
Enter fullscreen mode Exit fullscreen mode

The output of this code should look like this: An Image Depicting How Rust Implements Message Passing

Type safety

Like most popular programming languages, Rust and Haskell support fundamental data types such as strings, floats, integers, Boolean, char, etc.

Rust’s core design is implemented with safety and performance in mind. The Rust compiler checks type, uninitialized variables, and invalid memory access at compile time. The language implements its type system using type inference, static typing, and algebraic data type (ADT). Its strongly typed system prevents implicit type conversion and common runtime errors like null pointers and references.

Haskell is unique because of its strong and static type system, which checks for variable types at compile time. This action helps to detect type-related errors at compile time, improving code reliability. Every expression in Haskell is declared with a type. However, the Haskell compiler (GHC: Glasgow Haskell Compiler) can determine the type of every variable or expression when they are not ambiguous, a phenomenon known as type inference.

Variables and mutability

Rust variables are immutable by default, which means once you assign a value to a variable, you can’t change the value unless it is explicitly expressed. The code snippet below illustrates the concept of immutability in Rust:

fn main(){
  let log = 15;
  println(The value of log is: {log});
  log = 25;
  println(The value of log is: {log});
}
Enter fullscreen mode Exit fullscreen mode

The code above will give an immutability error message. To implement mutability in Rust, the keyword mut is used before the variable name. The example below illustrates how to implement mutability in Rust:

fn main(){
  let mut log = 15;
  log = 25;
}
Enter fullscreen mode Exit fullscreen mode

Like other functional programming languages, Haskell doesn’t support variable mutability. However, mutability can be implemented in Haskell using packages such as Data.IORef, Data.StRef, Control.Monad.Trans.State, and Control.Concurrent.STM.TVar. Generally speaking, to implement mutability in Haskell, you require a monad.

N.B., A monad is an algebraic structure in category theory. In Haskell, it is used to describe computations as sequences of steps and to handle side effects such as state and Input/Output (IO) operations.

Table overview of the differences between Rust and Haskell

Haskell Rust
Use cases Research, academia Web development, system programming
Paradigm Functional Multiparadigm
Memory safety Garbage collection Borrowing and ownership system
Concurrency Software transactional memory Multiple concurrency abstraction
Typing system Strong and static Static, inferred, strong

Conclusion

In this article, we learned the fundamentals of the Rust and Haskell programming languages, and some of their key performance features, including memory safety, concurrency, type safety, and variable immutability.


LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket Dashboard Free Trial Banner

LogRocket is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — start monitoring for free.

💖 💪 🙅 🚩
mangelosanto
Matt Angelosanto

Posted on October 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related