An open API service indexing awesome lists of open source software.

https://github.com/laugharne/rust_combinators

Combinators are higher-order functions that can combine or transform functions, enabling more abstract and concise code.
https://github.com/laugharne/rust_combinators

combinator combinators rust rust-lang

Last synced: 10 months ago
JSON representation

Combinators are higher-order functions that can combine or transform functions, enabling more abstract and concise code.

Awesome Lists containing this project

README

          

- [Theoretical Foundations](#theoretical-foundations)
- [Why Combinators in Rust?](#why-combinators-in-rust)
- [Basic Combinators in Rust](#basic-combinators-in-rust)
- [map](#map)
- [and_then](#and_then)
- [filter](#filter)
- [Advanced Combinators and Their Usage](#advanced-combinators-and-their-usage)
- [fold](#fold)
- [zip](#zip)
- [Combinators with Closures](#combinators-with-closures)
- [Practical Applications](#practical-applications)
- [Error Handling with Combinators](#error-handling-with-combinators)
- [map_err](#map_err)
- [or_else](#or_else)
- [Asynchronous Programming with Combinators](#asynchronous-programming-with-combinators)
- [Using and_then with Futures](#using-and_then-with-futures)
- [Combining Futures with join! and select!](#combining-futures-with-join-and-select)
- [Chaining and Composition](#chaining-and-composition)
- [Crafting your own combinator](#crafting-your-own-combinator)
- [Step 1: Understanding the Goal](#step-1-understanding-the-goal)
- [Step 2: Setting Up Your Rust Environment](#step-2-setting-up-your-rust-environment)
- [Step 3: Writing the Combinator](#step-3-writing-the-combinator)
- [Step 4: Implementing maybe_apply](#step-4-implementing-maybe_apply)
- [Step 5: Testing Your Combinator](#step-5-testing-your-combinator)
- [Step 6: Running Your Tests](#step-6-running-your-tests)
- [Step 7: Using Your Combinator in Practice](#step-7-using-your-combinator-in-practice)

> **Combinators** are higher-order functions that can combine or transform functions, enabling more abstract and concise code.

Let’s explore the theory behind combinators and do some combinators coding in Rust.

# Theoretical Foundations

In [functional programming](https://blog.devgenius.io/functional-programming-patterns-in-rust-bc14f3fe9626), a combinator is a function constructed solely from other functions without relying on variables or constants. The roots of combinators trace back to combinatory logic, a foundational theory in mathematical logic that predates computer science. In this context, combinators serve as the building blocks for constructing expressions and encapsulating computation patterns.

# Why Combinators in Rust?

Rust’s embrace of [functional programming](https://blog.devgenius.io/functional-programming-patterns-in-rust-bc14f3fe9626) concepts, such as higher-order functions, closures, and pattern matching, provides a fertile ground for using combinators. Combinators can enhance code readability, reduce boilerplate, and facilitate a declarative programming style. By leveraging combinators, Rust developers can express complex logic succinctly and compose reusable components effectively.

# Basic Combinators in Rust

Let’s start with some basic combinators that are frequently used in Rust programming.

## `map`

The `map` combinator applies a function to each element of an iterator, transforming them into a new form. It's widely used for data transformation tasks.
```rust
let nums = vec![1, 2, 3, 4];
let squares: Vec = nums.iter().map(|&x| x * x).collect();
println!("{:?}", squares); // Output: [1, 4, 9, 16]
```

## `and_then`

The `and_then` combinator is used with `Option` and `Result` types to chain operations that may return `Option` or `Result`. It's particularly useful for sequential operations where each step may fail or produce an optional value.

```rust
fn sqrt(x: f64) -> Option {
if x >= 0.0 { Some(x.sqrt()) } else { None }
}

let result = Some(4.0).and_then(sqrt);
println!("{:?}", result); // Output: Some(2.0)
```

## `filter`

The `filter` combinator is used to selectively include elements from an iterator based on a predicate function.

```rust
let nums = vec![1, 2, 3, 4, 5];

let even_nums: Vec = nums.into_iter().filter(|x| x % 2 == 0).collect();

println!("{:?}", even_nums); // Output: [2, 4]
```

# Advanced Combinators and Their Usage

As we delve deeper into Rust’s functional features, we encounter more sophisticated combinators that cater to complex scenarios.

## `fold`

The `fold` combinator aggregates elements of an iterator by applying a binary operation, starting from an initial value.

```rust
let nums = vec![1, 2, 3, 4];

let sum = nums.iter().fold(0, |acc, &x| acc + x);

println!("{}", sum); // Output: 10
```

## `zip`

The `zip` combinator pairs up elements from two iterators into a single iterator of tuples. It's useful for iterating over two sequences in parallel.

```rust
let nums1 = vec![1, 2, 3];

let nums2 = vec![4, 5, 6];

let zipped: Vec<_= nums1.iter().zip(nums2.iter()).collect();

println!("{:?}", zipped); // Output: [(1, 4), (2, 5), (3, 6)]
```

# Combinators with Closures

Closures in Rust are anonymous functions that can capture their environment. Combining closures with combinators allows for powerful and flexible code patterns.

```rust
let threshold = 2;

let nums = vec![1, 2, 3, 4];

let filtered: Vec = nums.into_iter().filter(|&x| x > threshold).collect();

println!("{:?}", filtered); // Output: [3, 4]
```

# Practical Applications

Combinators find practical applications in various domains, such as data processing, asynchronous programming, and functional reactive programming (FRP). For instance, in web development with frameworks like Actix or Rocket, combinators are used to compose middleware and request handlers in a declarative manner.

```rust
// Hypothetical example with a web framework
let app = App::new()
.route("/", HttpMethod::GET, |req| {
req.query("id")
.and_then(parse_id)
.map(fetch_data)
.map(Json)
});
```

In this example, the route handler chains several operations: extracting a query parameter, parsing it, fetching data based on the parsed ID, and finally wrapping the response in JSON.

# Error Handling with Combinators

Rust’s `Result` type is a powerful tool for [error handling](https://medium.com/coinmonks/rust-error-handling-in-practice-376d86ba12ca), representing a computation that might fail. Combinators like `map`, `and_then`, `or_else`, and `map_err` allow for elegant and concise [error-handling](https://medium.com/coinmonks/rust-error-handling-in-practice-376d86ba12ca) workflows.

## `map_err`

The `map_err` combinator is used to transform the error part of a `Result`. It's particularly useful when you need to convert errors from one type to another.

```rust
fn parse_number(num_str: &str) -> Result {
num_str.parse::().map_err(|e| e.to_string())
}

let result = parse_number("10");
println!("{:?}", result); // Output: Ok(10)
let result = parse_number("a10");
println!("{:?}", result); // Output: Err("invalid digit found in string")
```

## `or_else`

The `or_else` combinator provides a way to handle errors and possibly recover from them, allowing for fallback operations or error transformations.

```rust
fn try_parse_or_zero(num_str: &str) -> Result {
num_str.parse::().or_else(|_| Ok(0))
}

let result = try_parse_or_zero("20");

println!("{:?}", result); // Output: Ok(20)

let result = try_parse_or_zero("abc");

println!("{:?}", result); // Output: Ok(0)
```

# Asynchronous Programming with Combinators

Rust’s asynchronous programming model, based on futures and async/await, heavily relies on combinators to manage asynchronous computations. Combinators like `then`, `and_then`, `map`, and `map_err` are used with `Future`s to chain asynchronous operations in a non-blocking way.

## Using `and_then` with Futures

The `and_then` combinator can be used with futures to perform sequential asynchronous operations, where the output of one operation is the input to the next.

```rust
async fn fetch_url(url: &str) -> Result {
reqwest::get(url).await?.text().await
}

async fn process_url_data(url: &str) {
fetch_url(url)
.and_then(|data| async move {
println!("Fetched data: {}", data);
Ok(())
})
.await
.unwrap_or_else(|e| eprintln!("Error fetching data: {}", e));
}
// In an async runtime context
// process_url_data("http://example.com").await;
```

This example demonstrates fetching data from a URL and then processing that data asynchronously. The `and_then` combinator ensures that the data processing step only occurs if the data fetching step succeeds.

## Combining Futures with `join!` and `select!`

Rust also provides macros like `join!` and `select!` to work with multiple futures concurrently, allowing for parallel computation and race conditions handling.

- `join!` waits for all futures to complete and returns a tuple of their results.
- `select!` waits for the first future to complete and returns its result, cancelling the remaining futures.

```rust
async fn task_one() -i32 { 1 }

async fn task_two() -i32 { 2 }

async fn run_tasks() {
let (result_one, result_two) = join!(task_one(), task_two());
println!("Results: {}, {}", result_one, result_two);
let either = select! {
result_one = task_one().fuse() =result_one,
result_two = task_two().fuse() =result_two,
};
println!("First completed: {}", either);
}
// In an async runtime context
// run_tasks().await;
```

# Chaining and Composition

One of the key features of combinators is their ability to chain operations. Rust’s standard library provides numerous methods on types like `Option`, `Result`, and iterators, which are essentially built-in combinators.

Chaining allows for the composition of multiple operations in a concise and readable manner. For example, using `Option`'s `map` and `and_then` methods:

```rust
fn square(x: i32) ->i32 { x * x }

fn to_str(x: i32) ->Option { Some(x.to_string()) }

let result: Option = Some(2).map(square).and_then(to_str);
```

Here, `square` is applied to the value inside `Some`, and then `to_str` transforms the squared number into a string, all in a seamless chain of operations.

# Crafting your own combinator

## Step 1: Understanding the Goal

First, it’s essential to define what your combinator will do. For this guide, let’s create a combinator named `maybe_apply`. This combinator will work with the `Option` type and take two arguments: an `Option` and a function `F` that takes a `T` and returns a `U`. The combinator will apply the function to the value inside the `Option` if it is `Some`, otherwise, it will return `None`.

## Step 2: Setting Up Your Rust Environment

Ensure you have a Rust environment set up. You’ll need Rust installed on your system, which you can do by following the instructions on the official Rust website.

## Step 3: Writing the Combinator

Open your favorite editor or IDE, and start a new Rust project if necessary:

```bash
cargo new combinators_example
cd combinators_example
```

Now, open the `src/lib.rs` file (or `src/main.rs` if you prefer an executable) and start implementing the `maybe_apply` combinator.

## Step 4: Implementing `maybe_apply`

```rust
fn maybe_apply(option: Option, f: F) -> Option
where
F: FnOnce(T) -> U,
{
match option {
Some(value) =Some(f(value)),
None =None,
}
}
```

In this implementation:

- `T` and `U` are type parameters representing the types before and after applying the function `F`.
- `F` is constrained by `FnOnce(T) -U`, meaning it takes a `T` and returns a `U`. `FnOnce` is used because the function is consumed once called, suitable for closures that take ownership of their captured variables.
- The `match` expression checks if the `Option` is `Some` or `None`. If `Some`, it applies `f` to the contained value, otherwise, it propagates `None`.

## Step 5: Testing Your Combinator

To verify that `maybe_apply` works as intended, write some tests. Add the following code to the bottom of your `lib.rs` or `main.rs`:

```rust
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_maybe_apply_some() {
let result = maybe_apply(Some(5), |x| x * 2);
assert_eq!(result, Some(10));
}
#[test]
fn test_maybe_apply_none() {
let result: Option = maybe_apply(None, |x: i32| x * 2);
assert_eq!(result, None);
}
}
```

These tests cover the two possible scenarios: applying the function to a value inside `Some` and passing a `None` through unchanged.

## Step 6: Running Your Tests

Run your tests to ensure everything works as expected:

cargo test

If all goes well, you should see output indicating that both tests have passed.

## Step 7: Using Your Combinator in Practice

With `maybe_apply` tested and ready, you can now use it in your Rust applications. Here's a simple example:

```rust
fn main() {
let value = Some(10);
let doubled = maybe_apply(value, |x| x * 2);
println!("Doubled: {:?}", doubled); // Should print "Doubled: Some(20)"
}
```