crates.io docs.rs Maintenance

naan

deliciously succinct

naan is a functional programming prelude for the Rust language that is: * easy * useful * std- and alloc-optional * FAST - exclusively uses concrete types (no dynamic dispatch) meaning near-zero perf cost

Table of Contents

Higher-Kinded Types

HKT - What it is

In type theory, it can be useful to have language to differentiate between a concrete type (u8, Vec<u8>, Result<File, io::Error>) and a generic type without its parameters supplied. (Vec, Option, Result)

For example, Vec is a 1-argument (unary) type function, and Vec<u8> is a concrete type.

Kind refers to how many (if any) parameters a type has.

HKT - Why it's useful

In vanilla Rust, Result::map and Option::map have very similar shapes: ```rust impl Result { fn map(self, f: impl FnMut(A) -> B) -> Result; }

impl Option { fn map(self, f: impl FnMut(A) -> B) -> Option; } it would be useful (for reasons we'll expand on later) to have them both implement a `Map` trait: rust trait Map { fn map(self: Self, f: impl FnMut(A) -> B) -> Self; }

impl Map for Option { fn map(self, f: impl FnMut(A) -> B) -> Option { Option::map(self, f) } } `` but this code snippet isn't legal Rust becauseSelfneeds to be generic and in vanilla RustSelf` must be a concrete type.

HKT - How it's done

With the introduction of Generic associated types, we can write a trait that can effectively replace a "generic self" feature.

Now we can actually write the trait above in legal, stable rust: ```rust trait HKT { type Of; }

struct OptionHKT; impl HKT for OptionHKT { type Of = Option; }

trait Map where M: HKT = Self> { fn map(self, f: F) -> M::Of where F: FnMut(A) -> B; }

impl Map for Option { fn map(self, f: F) -> Option where F: FnMut(A) -> B { Option::map(self, f) } } ```

Currying

Currying - What it is

Currying is the technique where naan gets its name. Function currying is the strategy of splitting functions that accept more than one argument into multiple functions.

Example: rust fn foo(String, usize) -> usize; foo(format!("bar"), 12); would be curried into: rust fn foo(String) -> impl Fn(usize) -> usize; foo(format!("bar"))(12);

Currying - Why it's useful

Currying allows us to provide some of a function's arguments and provide the rest of this partially applied function's arguments at a later date.

This allows us to use functions to store state, and lift functions that accept any number of parameters to accept Results using Apply

EXAMPLE: reusable function with a stored parameter

```rust use std::fs::File;

use naan::prelude::*;

fn copyfileto_dir(dir: String, file: File) -> std::io::Result<()> { // ... # Ok(()) }

fn main() { let dir = std::env::var("DESTDIR").unwrap(); let copy = copyfiletodir.curry().call(dir);

File::open("a.txt").bind1(copy.clone()) .bind1(|| File::open("b.txt")) .bind1(copy.clone()) .bind1(|| File::open("c.txt")) .bind1(copy); }

/* equivalent to: fn main() { let dir = std::env::var("DEST_DIR").unwrap();

copy_file_to_dir(dir.clone(), File::open("a.txt")?)?;
copy_file_to_dir(dir.clone(), File::open("b.txt")?)?;
copy_file_to_dir(dir, File::open("c.txt")?)?;

} */ ```

EXAMPLE: lifting a function to accept Results (or Options)

```rust use std::fs::File;

use naan::prelude::*;

fn append_contents(from: File, to: File) -> std::io::Result<()> { // ... # Ok(()) }

fn main() -> std::io::Result<()> { Ok(append_contents.curry()).apply1(File::open("from.txt")) .apply1(File::open("to.txt")) .flatten() }

/* equivalent to: fn main() -> std::io::Result<()> { let from = File::open("from.txt")?; let to = File::open("to.txt")?; append_contents(from, to) } */ ```

Currying - How it's done

naan introduces a few new function traits that add ergonomics around currying and function composition; F1, F2 and F3. These traits extend the builtin function traits Fn and FnOnce with methods that allow currying and function composition.

(note that each arity has a "callable multiple times" version and a "callable at least once" version. The latter traits are denoted with a suffix of Once)

F2 and F2Once Definitions

``rust pub trait F2Once<A, B, C>: Sized { /// The concrete type thatcurry` returns. type Curried;

/// Call the function fn call1(self, a: A, b: B) -> C;

/// Curry this function, transforming it from /// /// fn(A, B) -> C /// to /// fn(A) -> fn(B) -> C fn curry(self) -> Self::Curried; }

pub trait F2: F2Once { /// Call the function with all arguments fn call(&self, a: A, b: B) -> C; }

impl F2 for F where F: Fn(A, B) -> C { /* / } impl F2Once for F where F: FnOnce(A, B) -> C { / */ } ```

Function Composition

Composition - What it is

Function composition is the strategy of chaining functions sequentially by automatically passing the output of one function to the input of another.

This very powerful technique lets us concisely express programs in terms of data that flows through pipes, rather than a sequence of time-bound statements:

```rust use naan::prelude::*;

struct Apple; struct Orange; struct Grape;

[derive(Debug, PartialEq)]

struct Banana;

fn appletoorange(a: Apple) -> Orange { Orange } fn orangetogrape(o: Orange) -> Grape { Grape } fn grapetobanana(g: Grape) -> Banana { Banana }

fn main() { let appletobanana = appletoorange.chain(orangetogrape) .chain(grapetobanana); asserteq!(appleto_banana.call(Apple), Banana) } ```

Typeclasses

Some of the most powerful & practical types in programming are locked behind a feature that many languages choose not to implement in Higher-Kinded Types.

Utilities like map, unwrap_or, and and_then are enormously useful tools in day-to-day rust that allow us to conveniently skip a lot of hand-written control flow.

Comparing and_then and map to their desugared equivalent

```rust use std::io;

fn networkfetchname() -> io::Result { Ok("harry".into()) } fn networksendmessage(msg: String) -> io::Result<()> { Ok(()) } fn globalstatestore_name(name: &str) -> io::Result<()> { Ok(()) }

// Declarative fn foo0() -> io::Result<()> { networkfetchname().andthen(|name| { globalstatestorename(&name)?; Ok(name) }) .map(|name| format!("hello, {name}!")) .andthen(networksend_message) }

// Idiomatic fn foo1() -> io::Result<()> { let name = networkfetchname()?; globalstatestorename(&name)?; networksend_message(format!("hello, {name}!")) }

// Imperative fn foo2() -> io::Result<()> { let name = match networkfetchname() { | Ok(name) => name, | Err(e) => return Err(e), };

match globalstatestore_name(&name) { | Err(e) => return Err(e), | _ => (), };

networksendmessage(format!("hello, {name}!")) } ```

A couple notes: - the "idiomatic" implementation is the most brief and scannable - the idiomatic and imperative implementations are more difficult to refactor due to scope sharing; imperative statements depend on the previous statements in order to be meaningful, while declarative expressions have little to no coupling to state or scope.

The value proposition of these typeclasses is that they allow us to think of types like Result, Option and Iterators as being abstract containers.

We don't need to know much about their internals to know how to use them effectively and productively.

This extremely simple but powerful metaphor allows us to solve some very complex problems with data structures that have a shared set of interfaces.

Functor

using a function to transform values within a container

Functor is the name we give to types that allow us to take a function from A -> B and effectively "penetrate" a type and apply it to some F<A>, yielding F<B> (a.fmap(a_to_b)).

🔎 This is identical to Result::map and Option::map.

🔎 There is a separate trait FunctorOnce which extends Functor to know that the mapping function will only be called once.

Functor is defined as: rust // 🔎 `Self` must be generic over some type `A` pub trait Functor<F, A> where F: HKT1<T<A> = Self> { // 🔎 given a function `A -> B`, // apply it to the values of type `A` in `Self<A>` (if any), // yielding `Self<B>` fn fmap<AB, B>(self, f: AB) -> F::T<B> where AB: F1<A, B>; }

Bifunctor

mapping types with 2 generic parameters

Bifunctor is the name we give to types that have 2 generic parameters, both of which can be mapped.

Bifunctor requires: * bimap * transforms T<A, B> to T<C, D>, given a function A -> C and another B -> D.

Bifunctor provides 2 methods: * lmap (map left type) * T<A, B> -> T<C, B> * rmap (map right type) * T<A, B> -> T<A, D>

🔎 There is a separate trait BifunctorOnce which extends Bifunctor to know that the mapping functions will only be called once.

Bifunctor is defined as: ``rust pub trait Bifunctor<F, A, B> where F: HKT2<T<A, B> = Self> { /// 🔎 In Result, this combinesmapandmap_err` into one step. fn bimap(self, fa: FA, fb: FB) -> F::T where FA: F1, FB: F1;

/// 🔎 In Result, this maps the "Ok" type and is equivalent to map. fn lmap(self, fa: FA) -> F::T where Self: Sized, FA: F1 { self.bimap(fa, |b| b) }

/// 🔎 In Result, this maps the "Error" type and is equivalent to map_err. fn rmap(self, fb: FB) -> F::T where Self: Sized, FB: F1 { self.bimap(|a| a, fb) } } ```

Semigroup and Monoid

Combining two values of a concrete type

Semigroup is the name we give types that support some associative combination of two values (a.append(b)).

🔎 Associative means a.append( b.append(c) ) must equal a.append(b).append(c).

Examples: * integer addition * 1 * (2 * 3) == (1 * 2) * 3 * integer multiplication * 1 + (2 + 3) == (1 + 2) + 3 * string concatenation * "a".append("b".append("c")) == "a".append("b").append("c") == "abc" * Vec<T> concatenation * vec![1].append(vec![2].append(vec![3])) == vec![1, 2, 3] * Option<T> (only when T implements Semigroup) * Some("a").append(Some("b")) == Some("ab") * Result<T, _> (only when T implements Semigroup) * Ok("a").append(Ok("b")) == Ok("ab")

Monoid extends Semigroup with an "identity" or "empty" value, that will do nothing when appended to another.

Examples: * 0 in integer addition * 0 + 1 == 1 * 1 in integer multiplication * 1 * 2 == 2 * empty string * String::identity() == "" * "".append("a") == "a" * Vec<T> * Vec::<u32>::identity() == vec![] * vec![].append(vec![1, 2]) == vec![1, 2]

These are defined as: ```rust pub trait Semigroup { // 🔎 Note that this can be any combination of 2 selves, // not just concatenation. // // The only rule is that implementations have to be associative. fn append(self, b: Self) -> Self; }

pub trait Monoid: Semigroup { fn identity() -> Self; } ```

Alt and Plus

Combining two values of a generic type

Alt is the name we give to generic types that support an associative operation on 2 values of the same type (a.alt(b)).

🔎 Alt is identical to Semigroup, but the implementor is generic.

🔎 alt is identical to Result::or and Option::or.

Examples: * Vec<T> * vec![1].alt(vec![2]) == vec![1, 2] * Result<T, _> * Ok(1).alt(Err(_)) == Ok(1) * Option<T> * None.alt(Some(1)) == Some(1)

Plus extends Alt with an "identity" or "empty" value, that will do nothing when alted to another.

🔎 Plus is identical to Monoid, but the implementor is generic.

Examples: * Vec<T> (Vec::empty() == vec![]) * Option<T> (Option::empty() == None)

These are defined as: ``rust // 🔎Selfmust be generic over some typeA`. pub trait Alt where Self: Functor, F: HKT1 = Self> { fn alt(self, b: Self) -> Self; }

pub trait Plus where Self: Alt, F: HKT1 = Self> { fn empty() -> F::T; } ```

Foldable

Unwrapping & transforming entire data structures

Types that are Foldable can be unwrapped and collected into a new value. Fold is a powerful and complex operation because of how general it is; if something is foldable, it can be folded into practically anything.

🔎 There is a separate trait FoldableOnce which extends Foldable to know that the folding function can only be called once.

Folding can be thought of as a series of steps: 1. Given some foldable F<T>, and you want a R * I have a Vec> and I want to sum the u32s that are Some, and discard the Nones 1. Start with some initial value of type R * I want a sum of u32s, so I'll start with zero. 1. Write a function of type Fn(R, T) -> R. This will be called with the initial R along with a value of type T from within F<T>. The function will be called repeatedly with the R returned by the last call until there are no more Ts in F<T>. * |sum_so_far, option_of_u32| sum_so_far + option_of_u32.unwrap_or(0) 1. This function will be called for every T contained in F<T>, collecting them into the initial value R you provided. * vec![Some(1), None, Some(2), Some(4)].fold(|sum, n| sum + n.unwrap_or(0)) == 7

Examples

Result to Option

```rust use naan::prelude::*;

fn passing() -> Result

fn failing() -> Result

assert_eq!(match passing() { | Ok(t) => Some(t), | _ => None, }, Some(0));

asserteq!(passing().fold1(|, t| Some(t), None), Some(0)); asserteq!(failing().fold1(|, t| Some(t), None), None); ```

Collapse a Vec

```rust use naan::prelude::*;

asserteq!(vec![1, 2, 3].foldl(|sum, n| sum + n, 0), 6); asserteq!(vec![2, 4, 6].foldl(|sum, n| sum * n, 1), 48); assert_eq!(vec!["a", "b", "c"].foldl(|acc, cur| format!("{acc}{cur}"), String::from("")), "abc"); ```

Foldable is defined as: ```rust pub trait Foldable where F: HKT1 = Self> { /// Fold the data structure from left -> right fn foldl(self, f: BAB, b: B) -> B where BAB: F2;

/// Fold the data structure from right -> left fn foldr(self, f: ABB, b: B) -> B where ABB: F2;

/// Fold the data structure from left -> right fn foldl_ref<'a, B, BAB>(&'a self, f: BAB, b: B) -> B where BAB: F2, A: 'a;

/// Fold the data structure from right -> left fn foldr_ref<'a, B, ABB>(&'a self, f: ABB, b: B) -> B where ABB: F2<&'a A, B, B>, A: 'a;

} ```

🔎 Foldable provides many additional methods derived from the required methods above. Full documentation can be found here. ```rust use naan::prelude::*;

fn is_odd(n: &usize) -> bool { n % 2 == 1 }

fn is_even(n: &usize) -> bool { n % 2 == 0 }

asserteq!(Some("abc".tostring()).fold(), "abc".tostring()); asserteq!(Option::::None.fold(), "");

let abc = vec!["a", "b", "c"].fmap(String::from);

asserteq!(abc.clone().fold(), "abc"); asserteq!(abc.clone().intercalate(", ".into()), "a, b, c".tostring()); asserteq!(vec![2usize, 4, 8].any(isodd), false); asserteq!(vec![2usize, 4, 8].all(is_even), true); ```

Lazy IO

License

Licensed under either of

  • Apache License, Version 2.0, (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
  • MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.