Expand description

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])

Re-exports

pub use compose::Chain;

Modules

Module for composition of computation. For example, the ability to chain a number of arbitrary computation bundles together.
Common utility stuff.
Utilities for building and finalising closure state ergonomically.

Structs

A closure containing bundled state, that can be called repeatedly on an shared reference to it’s internal state.
A closure containing bundled state, that can be generically mutated when generating output.
A closure containing bundled state, that takes the given arguments and produces the given output.

Traits

Replacement for Fn - representing a repeatable bundled computation without mutation, until the standard trait can be manually implemented. Designed to use the same API as Fn, even though it somewhat restricts dependency on internal computational state in returned references (which would be usable if we allowed the usage of a self-lifetime-parameter for a generic-associated-type).
Replacement for FnOnce - representing a repeatable bundled computation that mutates state - until that can be manually implemented. Designed to use the same API as FnMut, even though it somewhat restricts dependency on internal state in returned references (in particular, due to lack of a generic-associated-type parameterised on self-lifetime).
Replacement for FnOnce - representing a single bundled computation - until that can be manually implemented. Designed to use the same API.

Functions

Convert Co into Fn
Convert CoMut into FnMut
Convert CoOnce into FnOnce.
Start building a closure state.
Build a simple single-value closure state.