Functional Programming

Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.

Rust is not a purely functional language, but incorporates many features from functional programming. By default, variables in Rust are immutable. Rust treats functions as first-class citizens, meaning they can be passed as arguments to other functions, returned from functions, and assigned to variables. Closures, which are anonymous functions that can capture their environment, are also a powerful tool for functional programming in Rust. Other functional features include pattern matching, algebraic data types, and higher-order functions, such as map, filter, and fold.

The following explores some notable Rust crates that aid in functional programming:

  • itertools is often used with iterator chains, enhancing the standard iterator functions.
  • im replaces standard collections when immutability is required.
  • frunk is used for complex functional programming needs, especially those involving type-level programming.

Compose Iterators

itertools itertools-crates.io itertools-github itertools-lib.rs cat-algorithms cat-rust-patterns cat-no-std cat-no-std::no-alloc

itertools includes extra iterator adapters, functions and macros.

It offers a wide range of functions for combining, grouping, and manipulating iterators, for example itertools::zip_longest, itertools::group_by.

fn main() {
    use itertools::Itertools;
    use itertools::assert_equal;
    use itertools::chain;

    // Assert that two iterables produce equal sequences
    assert_equal("hello world".split(' '), "hello world".split(' '));

    // Chain
    let mut result: Vec<i32> = Vec::new();

    for element in chain(&[1, 2, 3], &[4]) {
        result.push(*element);
    }
    assert_eq!(result, vec![1, 2, 3, 4]);

    // Cloned
    assert_eq!(itertools::cloned(b"abc").next(), Some(b'a'));

    // Deduplicate
    let data = vec![1., 1., 2., 3., 3., 2., 2.];
    itertools::assert_equal(data.into_iter().dedup(), vec![1., 2., 3., 2.]);

    // `into_group_map`
    let data = vec![(0, 10), (2, 12), (3, 13), (0, 20), (3, 33), (2, 42)];
    let lookup = data.into_iter().into_group_map();

    assert_eq!(lookup[&0], vec![10, 20]);
    assert_eq!(lookup.get(&1), None);
    assert_eq!(lookup[&2], vec![12, 42]);
    assert_eq!(lookup[&3], vec![13, 33])
}

Create Immutable Data Structures with im

im-website im im-crates.io im-github im-lib.rs cat-data-structures

im provides immutable data structures, such as lists, maps, and sets. It facilitates functional programming by providing data structures that cannot be modified in place. Use it when you need to ensure that data does not change in unexpected ways.

use im::HashMap;
use im::HashSet;
use im::Vector;
use im::hashmap;
use im::vector;

// `im` provides a safe default choice for the most common kinds of data
// structures, allowing you to defer careful thinking about the right data
// structure for the job until you need to start looking for optimisations.

// The most obvious benefit of _immutable data structures_ - avoiding the
// accidental mutation of data - is already handled so well by Rust’s type
// system that it’s just not something a Rust programmer needs to worry about.
// Their most prominent advantage in Rust is therefore _structural sharing_. If
// two data structures are mostly copies of each other, most of the memory they
// take up will be shared between them. Therefore, making copies of an immutable
// data structure is cheap. `im` is thus often the right choice for larger data
// sets modified infrequently.

// Because `im` makes copies of shared nodes in its data structures before
// updating them, the values you store must implement `Clone`. If you want to
// store values for which cloning is expensive, or values that don’t implement
// `Clone`, you need to wrap them in `Rc` or `Arc`.

fn main() {
    // Immutable Vector (based on RRB trees),
    // i.e. a sequence of elements in insertion order.
    // Practically every operation is O(log n), except push/pop on both sides,
    // which will be O(1) amortized, and O(log n) in the worst case.

    let mut v1 = Vector::new();
    // Push values to the back of a vector.
    v1.push_back(1);
    v1.push_back(2);
    v1.push_back(3);

    // Clone the Vector and modify.
    // `im` uses _lazy cloning_. The initial `clone` is instant, and as you
    // modify the cloned data structure, it will clone chunks only where you
    // change them, so that if you change the entire thing you will
    // eventually have performed a full clone.
    let mut v2 = v1.clone();
    v2.push_back(4);

    println!("Original vector: {:?}", v1);
    println!("Modified vector: {:?}", v2);

    // Create a new vector with the value at index index updated.
    let v3 = v2.update(1, 10);
    println!("Updated vector: {:?}", v3);

    // Simpler: use the `vector!` macro.
    assert_eq!(5, vector![1, 2, 3, 4, 5].len());

    // The `im` maps - HashMap and OrdMap - generally perform similarly to their
    // equivalents in the standard library, but tend to run a bit slower on the
    // basic operations. On the other hand, they offer the cheap copy and
    // structural sharing between copies that you’d expect from immutable
    // data structures.

    // Let's explore the Immutable HashMap (based on hash array mapped tries).
    // Most operations on this map are O(logx n) for a suitably high x that it
    // should be nearly O(1) for most maps. Keys will need to implement
    // `Hash` and `Eq`.
    assert_eq!(
        3,
        hashmap! {
        1 => 11,
        2 => 22,
        3 => 33
        }
        .len()
    );

    // Or construct a hash map with a single mapping.
    let map0 = HashMap::unit("key0", 0);
    assert_eq!(map0.get("key0"), Some(&0));

    // Or use `insert`:
    let mut map1 = HashMap::new();
    map1.insert("key1", 100);
    map1.insert("key2", 200);

    // Clone the map and modify.
    let mut map2 = map1.clone();
    map2.insert("key3", 300);

    println!("Original map: {:?}", map1);
    println!("Modified map: {:?}", map2);

    // Immutable HashSet
    let mut set1 = HashSet::new();
    set1.insert(1);
    set1.insert(2);

    // Create a new set with a modification.
    let mut set2 = set1.clone();
    set2.insert(3);

    println!("Original set: {:?}", set1);
    println!("Modified set: {:?}", set2);
}

Use a general purpose sum type with either

either either-crates.io either-github either-lib.rs cat-data-structures cat-no-std

The enum Either with variants Left and Right is a general purpose sum type with two cases. This is useful for representing values that can take on one of two different forms, which is a common pattern in functional programming. Either has methods that are similar to Option and Result, and it also implements traits like Iterator. This crate also includes macros try_left!() and try_right!() to use for short-circuiting logic, similar to how the ? operator is used with Result.

Note that Either is general purpose. For describing success or error, use the regular Result enum.

// COMING SOON

Use Functional Programming Data Structures and Type-level Programming Tools with frunk

frunk frunk-crates.io frunk-github frunk-lib.rs

Frunk is a functional programming toolbelt for Rust. It provides developers with a number of functional programming data structures and type-level programming tools like HList (heterogeneous lists), Coproduct, Generic, LabelledGeneric, Validated, Monoid, Semigroup and friends. It is useful for complex data transformations and metaprogramming.

// // COMING SOON