Rust for Haskell Developers

We love Haskell, but we also love learning new languages. In this article, we want to show how to use your Haskell knowledge to write Rust code.

We’ll go through the concepts familiar to most Haskell developers, present a few gotchas, and cover these questions:

  • To what extent is FP possible in Rust? (On the scale from Java streams to doing Lambda Calculus on paper.)
  • Which Haskell concepts map well to Rust, and which don’t?
  • Are there monads in Rust?

This article is written to help Haskellers grasp common Rust concepts faster. For a more detailed comparison of languages, read our Rust vs. Haskell article.

Does Rust follow FP principles?

Rust Book says: “Rust’s design has taken inspiration from many existing languages and techniques, and one significant influence is functional programming”.

This section reviews if Rust adheres to basic functional programming principles.

Immutability

When we typically think about immutability, we think about values that get assigned only once and variables that never change. But this is only part of the picture.

Variables in Rust are immutable by default. We can make them mutable by using the mut keyword.

let immutable_x: i32 = 3;
    immutable_x = 1;
//  ^^^^^^^^^^^^^^^ error: cannot assign twice to immutable variable
let mut mutable_x = 3; // 3
mutable_x = 1;         // 1
mutable_x += 4;        // 5

But we get support from the Rust’s ownership model and borrow checker, which guarantee safety in the presence of mutability. Rust has strict rules about borrowing, which affect what operations the owner of the item can perform. The rules guarantee that only one thing has write access to a variable, and whenever you mutate, you don’t have to worry about breaking any reader of the data.


💡 You know how ST allows local mutation?


For example, we can’t suddenly mutate the collection that we’re iterating over:

let mut ints = vec![1, 2, 3];

for i in &ints {
//       ^^^^^  immutable borrow occurs here
    println!("{}", i);
    ints.push(34);
//  ^^^^^^^^^^^^^ mutable borrow occurs here
}

// error[E0502]: cannot borrow `ints` as mutable because it is also borrowed as immutable

There is a clear separation between mutable and immutable code, and mutability is contained.

As a result, there is no major use case for persistent data structures in Rust.


💡 A few crates provide immutable collections, for example, im and rpds.


Purity

Pure functions don’t have side effects. In Rust, we can perform arbitrary side effects anywhere and anytime, so don’t expect referential transparency.


💡 A quick reminder: What is referential transparency?

If an expression is referentially transparent, we can replace it with its value without changing the program’s behavior. For instance, the following snippets should be the same:

let a = <expr>
(a, a)
(<expr>, <expr>)

This means you have to keep an eye on your Rust code. The code might not behave as expected, and refactoring might break the logic.

let a: i32 = {
    println!("Boom");
    42
};

let result = a + a;
println!("Result is {result}");

// Prints:
// Boom
// Result is 84

If we try to inline, we get different results:

let result = {
    println!("Boom");
    42
} + {
    println!("Boom");
    42
};
println!("Result {result}");

// Prints:
// Boom
// Boom
// Result is 84

💡 Also note that you can’t easily copy-paste code around because the scope might affect allocation management.


Totality

Here’s the twist: Rust is way better at totality than Haskell – for instance, Rust doesn’t allow partial field selectors or non-exhaustive pattern matches, and the standard library has fewer partial functions. You can safely get the head of a list, wrapped in Rust’s variant of the Maybe type!

let empty: Vec<i32> = vec![];
println!("The head is {:?}", empty.first());
// Prints: The head is None

Idiomatic FP in Rust

Functional Rust is frequently just proper, idiomatic Rust. On top of that, thinking in terms of expressions, by-default immutable variables, iterator combinators, Option/Result, and higher-order functions makes Rust appealing to functional programmers.

ADTs and pattern matching

We use struct and enum to declare data types in Rust.

Product types

In Rust, structs represent product types. They come in two forms:

// A tuple struct has no names:
struct SimpleMushroom(String, i32);

// Creating a tuple struct:
let simple = SimpleMushroom("Oyster".to_string(), 7);

// A struct has named fields:
#[derive(Debug)]
struct Mushroom {
    name: String,
    price: i32,
}

// Creating an ordinary struct:
let mushroom = Mushroom {
    name: "Oyster".to_string(),
    price: 7,
};

println!("It costs {}", mushroom.price);
// Prints: "It costs 7"

💡 #[derive(Debug)] is similar to deriving Show.


If we want to update a struct as we do in Haskell, we can use struct update syntax to copy and modify it:

let mushroomflation = Mushroom {
    price: 12,
    ..mushroom
};

println!("{:?}", mushroomflation);
// Prints: Mushroom { name: "Oyster", price: 12 }

But it’s more idiomatic to modify the fields of mutable structs:

let mut mushroom = Mushroom {
    name: "Porcini".to_string(),
    price: 75,
};

mushroom.price = 100;

println!("{:?}", mushroom);
// Prints: Mushroom { name: "Porcini", price: 100 }

Sum types

Enums represent sum types.

#[derive(Debug)]
enum Fungi {
    Edible { name: String, choice_edible: bool },
    Inedible { name: String },
}

Rust doesn’t allow accessing a partial field, such as the ones on an enum.

let king = Fungi::Edible {
    name: "King Bolete".to_string(),
    choice_edible: true,
};

println!("{}", king.name)
//                  ^^^^
// error[E0609]: no field `name` on type `Fungi`

Pattern matching

Instead, we can use pattern matching. For example, we can turn the value into a string.

fn to_string(fungi: Fungi) -> String {
    match fungi {
        Fungi::Edible { name, choice_edible } if choice_edible => {
            format!("{name} is tasty!")
        }
        Fungi::Edible { name, .. } => {
            format!("{name} is edible.")
        }
        Fungi::Inedible { name } => {
            format!("{name} is inedible!")
        }
    }
}

let king = Fungi::Edible {
    name: "King Bolete".to_string(),
    choice_edible: true,
};

println!("{}", to_string(king));
// King Bolete is tasty!

Note that Rust is stricter than Haskell about non-exhaustive patterns.

fn to_string(fungi: Fungi) -> String {
    match fungi {
//        ^^^^^
        Fungi::Inedible { name } => {
            format!("{name} is inedible!")
        }
    }
}
// error[E0004]: non-exhaustive patterns:
// pattern `Fungi::Edible { .. }` not covered

Option and Result

Option corresponds to Maybe:

// Defined in the standard library
enum Option<T> {
    None,
    Some(T),
}

let head = ["Porcini", "Oyster", "Shiitake"].get(0);
println!("{:?}", head);
// Prints: Some("Porcini")

Result corresponds to Either:

// Defined in the standard library
enum Result<T, E> {
    Ok(T),
    Err(E),
}

There are no monads and no do-notation. We can use unwrap_or() from Option or unwrap_or() from Result to safely unwrap values.

let item = [].get(0).unwrap_or(&"nothing");
println!("I got {item}");
// Prints: I got nothing

let result = Err::<i32, &str>("Out of mushrooms error").unwrap_or(0);
println!("I got {result}");
// Prints: I got 0

Note that there are also unwrap_or_else() and unwrap_or_default().

We can also use the and_then() method to chain (or bind) multiple effectful functions:

let inventory = vec![
    "Shiitake".to_string(),
    "Porcini".to_string(),
    "Oyster".to_string(),
];

let optional_head = inventory.first().and_then(|i| i.strip_prefix("Shii"));

println!("{:?}", optional_head);
// Prints: Some("take")

Also, Rust provides the question mark operator (?) to deal with sequences of Result or Option.

use std::collections::HashMap;

fn order_mushrooms(prices: HashMap<&str, i32>) -> Option<i32> {
    let donut_price = prices.get("Porcini")?; // early return if None
    let cake_price = prices.get("Oyster")?; // early return if None
    Some(donut_price + cake_price)
}

let prices = HashMap::from([("Porcini", 75), ("Oyster", 7)]);

let total_price = order_mushrooms(prices);
println!("{:?}", total_price);
// Prints: Some(82)

An expression that ends with ? either results in the unwrapped success value or short-circuits on failure.

For example, if looking up one of the mushrooms fails, the whole function fails:

let prices = HashMap::from([("Oyster", 7)]);

let total_price = order_mushrooms(prices);
println!("{:?}", total_price); 
// Prints: None

Newtypes

We can use a tuple struct with a single field to make an opaque wrapper for a type. Newtypes are a zero-cost abstraction – there’s no runtime overhead.

#[derive(Debug)]
struct MushroomName(String);

When we want to hide the internals, we can expose methods for converting to and from a newtype. For example:

impl MushroomName {
    pub fn new(s: String) -> MushroomName {
        MushroomName(s)
    }
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

let shiitake = MushroomName::new("Shiitake".to_string());
println!("{:?}", shiitake);
// MushroomName("Shiitake")

println!("{}", shiitake.as_str());
// Shiitake

We can also use traits (covered in the next section) to add this behavior: FromStr and Display.


💡 If the underlying type is not a string, there are other convenient traits you can use: TryFrom<T> and Into<T>.


Polymorphism

Rust traits are siblings of Haskell typeclasses.

We can implement the Display trait for MushroomName, which gives us the to_string() method and the ability to pretty-print values using println! and format!

use std::fmt;
impl fmt::Display for MushroomName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

let shiitake: MushroomName = MushroomName::new("Shiitake".to_string());
let shiitake_str: String = shiitake.to_string();
println!("Mushroom: {shiitake}");
// Prints: Mushroom: Shiitake

We can also implement FromStr , which allows us to use parse() and from_str().

use std::str::FromStr;
impl FromStr for MushroomName {
    type Err = Box<dyn std::error::Error>;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(MushroomName(s.to_string()))
    }
}

let shiitake = MushroomName::from_str("Shiitake");
println!("{:?}", shiitake);
// Prints: Ok(MushroomName("Shiitake"))

let shiitake = "Shiitake".parse::<MushroomName>();
println!("{:?}", shiitake);
// Prints: Ok(MushroomName("Shiitake"))

💡 We can derive trait implementations for enums and structs. We can derive all the standard traits, such as Debug for showing values, PartialEq and Eq for comparing, PartialOrd and Ord for ordering, Hash for hashing, etc.


Like with typeclasses, functions can rely on traits. For instance, parse() depends on the FromStr trait: it can parse into any type that implements FromStr.

pub fn parse<F: FromStr>(&self) -> Result<F, F::Err>

In Rust, we use trait bounds to restrict generics: <F: FromStr> declares a generic type parameter with a trait bound (similar to using constraints to limit type variables).

Using compile-time generics (which we just call generics) is similar to using types with type variables in Haskell.

Functions and closures

Lambdas and closures

Here are a couple of lambdas (note that you don’t have to specify their types):

let bump = |i: f64| i + 1.0;
println!("{}", bump(1.2));
// Prints: 2.2

let bump_inferred = |i| i + 1.0;
println!("{}", bump_inferred(1.2));
// Prints: 2.2

We can use closures to capture values from the scope/environment in which they’re defined. For example, we can define a simple closure that captures the outside variable:

|x| x + outside

In Rust, each value has a lifetime. Because closures capture environment values, we must deal with their ownership and lifetimes.

For example, let’s write a function that returns a greeting function:

fn create_greeting() -> impl Fn(&str) -> String {
    let greet = "Hello,";
    move |name: &str| format!("{greet} {name}!")
}

let greeting_function = create_greeting();

println!("{}", greeting_function("Rust"));
// Prints: Hello, Rust!

We use move to force the closure to take ownership of the greet variable. If we forget to use move, we get an error.

fn create_greeting() -> impl Fn(&str) -> String {
    let greet = "Hello,".to_string();
    |name: &str| format!("{greet} {name}!")
}
error[E0373]: closure may outlive the current function, but it borrows `greet`, which is owned by the current function
   |
   |         |name: &str| format!("{greet} {name}!")
   |         ^^^^^^^^^^^^           ----- `greet` is borrowed here
   |         |
   |         may outlive borrowed value `greet`
   |
note: closure is returned here
   |
   |         |name: &str| format!("{greet} {name}!")
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `greet` (and any other referenced variables), use the `move` keyword
   |
   |         move |name: &str| format!("{greet} {name}!")
   |         ++++

Another important thing is there are three types of closures. How a closure captures and handles values from the environment determines which traits (one, two, or all three) it implements and how the closure can be used.

  • FnOnce can be called once.
  • FnMut can be called more than once and may mutate state.
  • Fn can be called more than once, with no state mutation.

In Rust, each function or closure has a unique type; for example, the function fn hello() {} has the unique type fn() {hello}. In other words, two functions with the same signatures have different types.

If you want to return a different closure naively, you get a compiler error:

fn add_or_subtract(is_positive: bool, x: f64) -> impl Fn(f64) -> f64 {
    if is_positive {
        move |y: f64| x + y
    } else {
        move |y: f64| x - y
//      ^^^^^^^^^^^^^^^^^^^ expected closure, found a different closure
    }
}
// error[E0308]: `if` and `else` have incompatible types
// = note: no two closures, even if identical, have the same type
// = help: consider boxing your closure and/or using it as a trait object

As the compiler suggests, we have to box our closures:

fn add_or_subtract(is_positive: bool, x: f64) -> Box<dyn Fn(f64) -> f64> {
    if is_positive {
        Box::new(move |y: f64| x + y)
    } else {
        Box::new(move |y: f64| x - y)
    }
}

println!("{}", add_or_subtract(true, 1.2)(1.0));
// Prints: 2.2

Currying and partial application

We can implement curried functions in Rust, but it’s not idiomatic.

fn add(x: f64) -> impl Fn(f64) -> f64 {
    move |y| x + y
}

let bump = add(1.0);

println!("{}", bump(1.2)); 
// Prints: 2.2

💡 Some crates make it easier; for instance, we can use partial_application:

use partial_application::partial;

fn add(x: f64, y: f64) -> f64 {
    x + y
}

let bump2 = partial!(add => 1.0, _);

println!("{}", bump(1.2)); 
// Prints: 2.2

Function composition

We can technically create a function for this in Rust, but the concept isn’t commonly used and is not part of the standard library.

On the other hand, Rust supports method call syntax and method chaining. For example, we can call map multiple times on an Option:

let before = Some("body");
let after = before
    .map(|x| x.to_uppercase())
    .map(|x| x + "!")
    .unwrap_or("default".to_string());

println!("{after}")
// Prints: BODY!

Iterators

Every functional programmer should feel at home when using iterators in Rust. They allow writing declarative, lazy, and composable operations on collections.

The Iterator trait defines a standard interface for iterating over elements, and many of the standard collections in Rust implement this trait. On top of that, we can use IntoIterator and FromIterator traits to convert between collections and iterators.


💡 Similar to three closure types, there are three ways to create an iterator:

  • iter() iterates over immutable references;
  • iter_mut() iterates over mutable references;
  • into_iter() iterates over movable values (the collection is moved, and you can no longer use the original one).

Collections may implement one or more of the three.


The simplest and least Haskell way of using iterators is the for loop, which is syntactic sugar for looping over elements provided by the IntoIterator implementation.

use std::collections::HashMap;

let prices = HashMap::from([("Porcini", 75), ("Oyster", 7)]);

for (mushroom, prices) in prices.iter() {
    println!("{mushroom}: ${prices}");
}
// Prints:
// Oyster: $7
// Porcini: $75

But this is not why we are here. Let’s map something! First, let’s prepare the data – imagine we have a collection of mushroom prices.

struct Mushroom {
    name: String,
    price: i32,
}

let mushrooms = vec![
    Mushroom {
        name: "Porcini".to_string(),
        price: 75,
    },
    Mushroom {
        name: "Oyster".to_string(),
        price: 7,
    },
    Mushroom {
        name: "Shiitake".to_string(),
        price: 12,
    },
];

We can filter and get modified names of mushrooms that we can afford:

let loud_names: Vec<String> = mushrooms
    .iter()
    .filter(|m| m.price < 15)
    .map(|m| m.name.to_uppercase())
    .collect();

println!("{:?}", loud_names);
// Prints: ["OYSTER", "SHIITAKE"]

We use collect() to transform an iterator into a collection. Iterators are lazy – they do nothing if they are not consumed. We must also add a type annotation (loud_names: Vec<String>) to specify the target collection and help the type inference.


💡 The following code creates an iterator but doesn’t use it.

let mushrooms = vec!["Porcini", "Oyster", "Shiitake"];
mushrooms.iter().map(|m| m.to_uppercase());

The compiler produces a warning:

warning: unused `std::iter::Map` that must be used
   |
   |     mushrooms.iter().map(|m| m.to_uppercase());
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: iterators are lazy and do nothing unless consumed

We can use the bind’s cousin function flat_map() (which is map(f).flatten()) to smoosh the strings (or other collections) into one:

let inventory: String = mushrooms
    .iter()
    .flat_map(|Mushroom { name, price: _ }| ["- ", name, "\n"])
    .collect();

println!("{}", inventory)
// Prints multiline string:
// * Porcini
// * Oyster
// * Shiitake

collect() is not the only way to consume an iterator; for example, we can use count() (Haskell length) to count the number of iterations or use one of the methods to fold the collection. We can use reduce() (Haskell foldl1) to reduce the prices into a single one:

let total_price = mushrooms.iter().map(|m| m.price).reduce(|acc, e| acc + e);

println!("{:?}", total_price);
// Prints: Some(94)

If the iterator is empty, reduce() returns None. We can use left-associative fold() (Haskell foldl) or right-associative rfold() (Haskell foldr) with initial element, which can be illustrated using strings:

let l_result = mushrooms
    .iter()
    .fold("0".to_string(), |acc, m| format!("({} + {acc})", m.name));
println!("{:?}", l_result);
// Prints: "(Shiitake + (Oyster + (Porcini + 0)))"

let r_result = mushrooms
    .iter()
    .rfold("0".to_string(), |acc, m| format!("({} + {acc})", m.name));
println!("{:?}", r_result);
// Prints: "(Porcini + (Oyster + (Shiitake + 0)))"

💡 Note that Rust is not Haskell, don’t expect the operations to behave precisely the same.


Iterators support a whole bunch of operations that we all use and love in Haskell: any(), all(), find(), filter_map() (Haskell mapMaybe), intersperse(), last(), skip() (Haskell drop), sum(), take(), take_while(), zip(), etc.

Okay, I know what you are thinking. What about traverse? There is no traverse() in Rust!

But don’t give up yet. There is type inference and collect(). To illustrate this, let’s introduce another type of mushroom and an error to track inedible ones.

struct WildMushroom {
    name: String,
    is_edible: bool,
}

#[derive(Debug)]
struct InedibleError;

let mushrooms = vec![
    WildMushroom {
        name: "King Bolete".to_string(),
        is_edible: true,
    },
    WildMushroom {
        name: "Oyster".to_string(),
        is_edible: true,
    },
    WildMushroom {
        name: "Amanita".to_string(),
        is_edible: false,
    },
];

Let’s collect the names of only edible mushrooms:

let total_price: Vec<Result<String, InedibleError>> = mushrooms
    .into_iter()
    .map(|m| {
        if m.is_edible {
            Ok(m.name)
        } else {
            Err(InedibleError)
        }
    })
    .collect();

println!("{:?}", total_price);
// Prints: [Ok("King Bolete"), Ok("Oyster"), Err(InedibleError)]

Nothing special so far – we get a vector of results: Vec<Result<String, InedibleError>>.

But watch this! If we slightly change the type, it behaves like traverse.

let total_price: Result<Vec<String>, InedibleError> = mushrooms
    .into_iter()
    .map(|m| {
        if m.is_edible {
            Ok(m.name)
        } else {
            Err(InedibleError)
        }
    })
    .collect();

println!("{:?}", total_price);
// Prints: Err(InedibleError)

The code is the same, but the result is Result<Vec<String>, InedibleError>. And we get the short-circuiting behavior!


💡 To learn more about collect(), check out the FromIterator documentation.


If there are no errors, we get back Ok():

let mushrooms = vec![
    WildMushroom {
        name: "King Bolete".to_string(),
        is_edible: true,
    },
    WildMushroom {
        name: "Oyster".to_string(),
        is_edible: true,
    },
];

let total_price: Result<Vec<String>, InedibleError> = mushrooms
    .into_iter()
    .map(|m| {
        if m.is_edible {
            Ok(m.name)
        } else {
            Err(InedibleError)
        }
    })
    .collect();

println!("{:?}", total_price);
// Prints: Ok(["King Bolete", "Oyster"])

💡 Bad news: We can have side effects in Rust.

Good news: We don’t need IO, and we don’t need traverse to deal with it.


Associated types

Rust also supports associated types. We can use them to group multiple types; for instance, the Holdable type and the type of Item it holds:

trait Holdable {
    type Item;

    fn add_item(&mut self, item: &Self::Item);

    fn pop_Item(&mut self) -> Option<Self::Item>;
}

💡 Self::Item means that the Item is defined in the implementation for the type Self.


Item is determined by the trait implementation. After the compiler figures out the concrete Holdable implementation, it knows which type to use for Item. For instance, in the following snippet, we see that a Basket will always hold Mushrooms:

struct Basket;

struct Mushroom(String);

impl Holdable for Basket {
    type Item = Mushroom;

    fn add_item(&mut self, item: &Mushroom) {
        /* do nothing for now */
    }

    fn pop_item(&mut self) -> Option<Mushroom> {
        None
    }
}

And the users can abstract over a given Holdable – they don’t have to deal with the Item type.

// No `Item` in the type signature:
fn size<H: Holdable>(holdable: &H) -> u32 {
    0
}

Generic associated types

Rust 1.65.0 introduced generic associated types, which allow specifying generics (lifetimes, types, and const) on associated types in traits. In other words, we can associate type constructors with traits, an incremental step towards higher-kinded types.

For example, we can make a new version of Holdable that allows the pop_item function to return an item borrowed from self.

trait Holdable {
    type Item<'a>
    where
        Self: 'a;

    fn add_item<'a>(&'a mut self, item: &Self::Item<'a>);

    fn pop_item<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

We don’t do that over here

Monads and friends

Rust doesn’t support higher-kinded types (yet), and it’s more complex to implement concepts such as monads and their friends. Instead, Rust provides alternative approaches to solve the problems that these typeclasses solve in Haskell:

  • If you want to deal with optionality or error handling, you can use Option/Result with the ? operator.
  • If you want to deal with async code – async/await.
  • etc.

Developers come up with multiple ways to emulate higher-kinded types and implement them. If you search for crates that have monads, there are dozens of them, but none of them are established or widely used. There is nothing like Arrow in Kotlin.


💡 Do you miss do-notation?

You can already write imperative code in Rust, so you don’t need a specific syntax. However, various crates emulate do-notation; for example, one is called do_notation.


The frunk crate

There is no “full” functional programming crate, but it doesn’t mean there are no FP libraries in Rust. For instance, frunk provides some FP tools, such as Validated, Monoid, and HList. Let’s look at a couple of examples.

To start, we can use monoids:

use frunk::Monoid;

let empty_vec: Vec<i32> = Monoid::empty();
println!("{:?}", empty_vec);
// Prints: []

let empty_str: String = Monoid::empty();
println!("{:?}", empty_str);
// Prints: ""

Moreover, the library provides monoid’s combine_all (mconcat), which we can use, for example, with the Product wrapper to combine a list of options into one:

let options: Vec<Option<Product<i32>>> =
        vec![Some(Product(2)), None, Some(Product(3))];

let one_option = monoid::combine_all(&options);
println!("{:?}", one_option)
// Prints: Some(Product(6))

Validated is a great way to handle errors and a great way to lure people into FP (shout out to all the Scala developers who brought the cats library to do validation).


💡 A Validated is either an Ok holding an HList or an Err holding a vector of errors.


For example, we can use it to parse mushrooms:

use frunk::prelude::IntoValidated;
use frunk::Validated;
use frunk::{hlist_pat, HList};

#[derive(Debug)]
struct Mushroom {
    name: String,
    price: i32,
}

// Name and price parser return errors
fn parse_name() -> Result<String, String> {
    Err("The name parser isn't working".to_string())
}

fn parse_price() -> Result<i32, String> {
    Err("The price parser isn't working".to_string())
}

// Convert each result into validated and combine them
let validated_inputs: Validated<HList!(String, i32), String> =
    parse_name().into_validated() + parse_price().into_validated();

// Convert validated inputs to the mushroom result
let mushroom: Result<Mushroom, Vec<String>> = validated_inputs
    .into_result()
    .map(|hlist_pat!(name, price)| Mushroom { name, price });

println!("{:?}", mushroom);
// Prints: Err(["The name parser isn't working", "The price parser isn't working"])

Because both of the parsers return an error, the result contains a vector with two errors.

Conclusion

Standard library functions and datatypes, iterators, and other Rust concepts often tend to look more idiomatic when written in a functional style. We can enjoy writing code free of both immutability and impurities.

To a Haskell developer, Rust provides support for fundamental concepts and functionality, including monad-like types (e.g., Option, Result, Iterator) and operations: bind-like, map-like, and other methods.

But Rust is not Haskell and not a functional language. There are dozens of Rust blog posts that implement monads, GADTs, etc., and dozens of FP crates, but most of these are proof of concepts and need to be established.

To read more articles about Rust and Haskell, follow us on Twitter and subscribe to our newsletter via the form below.

Haskell courses: everyday extensions
More from Serokell
Haskell in Production: CaribouHaskell in Production: Caribou
Developing GHCDeveloping GHC
Parsing Typed eDSLParsing Typed eDSL