#closures #no-std #functional #api

no-std nclosure

Provides composable, nameable closure types with separated states and functionality for use in APIs where anonymous types are unavailable

1 unstable release

Uses new Rust 2021

0.1.0 Oct 11, 2022

#470 in Rust patterns

MIT license

29KB
298 lines

nclosure

nclosure is a library designed to ease the use of closures and generic, composable computation in APIs where anonymous trait objects, like impl FnOnce cannot currently be placed.

In particular, it provides an ergonomic method of defining closures such that the state embedded within the closure can be separated from the raw fn pointer, which can be encoded in the type system with a named type.

Furthermore, it provides versions of the FnOnce, FnMut and Fn traits that can be implemented in a custom manner upon structures. This allows for the encoding of composed computation into nameable types, of which this crate provides some, hence allowing for it's use as a return type in areas of code where anonymous types are not yet allowed. https://github.com/rust-lang/rust/issues/29625

Closure types

The primary function of this crate is to enable using traits with functions that provide computations as output or that may require specific, nameable representations of computation as input for storage.

In this crate, a closure refers to a bundle of some specific state, along with a separate function pointer. Standard rust closures are also essentially this, but each closure is an entirely new, unnameable type in Rust, and hence difficult to put into uniform storage without allocation/boxing/dynamic dispatch/etc. The closures in the nclosure crate are uniform types such that you can store different function pointers or state values in one closure type.

[ClosureOnce]

This type is a closure that can only be called once, and it consumes it's inner state. Remember, though - the inner state of the closure is able to hold things like references just fine.

It is what you almost always want for any one-shot computation.

[ClosureMut]

This is a closure that can be called repeatedly to generate values, mutating it's inner state - this means that calling it requires exclusive references to the closure. If you just need to hold a reference for a single computation, [ClosureOnce] will do just fine.

[Closure]

This holds a bare computation that can be called repeatedly without holding an exclusive reference to it's internal state, including stuff like cross threads. This is the most general form of closure - it could own it's state or it could hold some other reference.

Traits

As a consequence of the current inability to define your own [Fn] , [FnMut] , and [FnOnce] implementations on custom types - like the various closure types defined here - this crate also defines it's own custom computation types that are a superset of the built-in types as a workaround (in particular, [Co] , [CoMut] and [CoOnce] , which are automatically implemented for the Rust core traits).

These can be freely converted to the Rust core impl-trait types using [as_fn_once] , [as_fn_mut] , and [as_fn] .

Examples

Section containing example usages of this crate.

The benefit of using this crate is that the resulting computation type is nameable solely in terms of inner state, arguments, and return values - as opposed to being an opaque impl FnMut type.

This enables several things. Not only does it make it easier to define storage for closures, it also allows easier usage of automatically derived (or otherwise) traits - for instance, with respect to thread synchronisation and Co/CoMut/CoOnce.

In particular, the compositional methods provided by this trait preserve the repeatability (Co vs CoMut vs CoOnce) of their generic parameters, which means that a single method can preserve the repeatability of arbitrary composition of functions because the resulting composition is itself a named type rather than an anonymous impl Fn/impl FnMut/impl FnOnce type.

Closure Constructions

Some examples of how to ergonomically construct nameable closures using this crate.

Counter

The first example is the construction of a closure that generates increasing values, illustrating the simple means of constructing a closure state.

use nclosure::prelude::*;

/// Count from the given number upwards. 
pub fn count_from(a: usize) -> ClosureMut<usize, (), usize> {
    single_closure_state(a)
        .finalise_mut(|count, ()| {
            let result = *count;
            *count += 1;
            result
        })
}


// Note that unfortunately we have to provide an empty tuple argument, because the computation traits have their arguments as a tuple.
let mut counter = count_from(5).as_fn_mut();
let first = counter(());
let second = counter(());
let third = counter(());

assert_eq!((first, second, third), (5, 6, 7))

Fibonacci

This demonstrates the construction of a more complex closure state using the fluent API, to produce the fibonacci sequence.

use nclosure::prelude::*;

/// Make a function that generates the Fibonacci sequence, starting from 0
pub fn fibonacci() -> ClosureMut<(usize, usize), (), usize> {
    // Note the way this fluently creates a tuple
    closure_state().and(0).and(1)
        // And the way this fluently accesses it by mutable reference to each component 
        // with names.
        // If you keep the names of state parameters aligned with the arguments to
        // this function you can get near-native closure ergonomics feel.
        .finalise_mut(|(current, next), ()| {
            let result = *current;
            (*current, *next) = (*next, *next + *current);
            result
        })
}

let mut fibgen = fibonacci().as_fn_mut();
let vals = [fibgen(()), fibgen(()), fibgen(()), fibgen(()), fibgen(()), fibgen(())];
assert_eq!(vals, [0usize, 1, 1, 2, 3, 5]);

Closure Composition

One of the useful aspects of this crate is the clean composition of computation and state, in a manner that produces nameable types. This section illustrates methods for doing that.

Chaining Closures (Squared Counter)

This illustrates an example of chaining closures together in a nameable fashion.

use nclosure::prelude::*;
use core::ops::Mul;

pub fn counter() -> ClosureMut<usize, (), usize> {
    single_closure_state(0usize)
        .finalise_mut(|ctr, ()| {
            let res = *ctr;
            *ctr += 1;
            res
        }) 
} 

/// Square the output of a function 
/// Note here that the output type is actually *named*, which enables 
/// some very useful properties.  In particular, if the input computation 
/// implements `Co` or `CoMut` instead of just `CoOnce`, then the 
/// composed computation will also implement those traits (at least as 
/// long as the chained computations also implement them).
pub fn square_result<Args, IC: CoOnce<Args>>(input_computation: IC) 
    -> nclosure::Chain<(IC, fn(IC::Output) -> <IC::Output as Mul>::Output)>  
    where IC::Output: Mul + Copy {
   input_computation.and_then(|v| v * v) 
}

// Note how when the counter is `CoMut`, the resulting function is also.
let mut squared_ctr = square_result(counter()).as_fn_mut();
let vals = [squared_ctr(()), squared_ctr(()), squared_ctr(()), squared_ctr(())];
assert_eq!(vals, [0usize, 1, 4, 9])

Dependencies