1 unstable release
0.1.0 | Oct 11, 2022 |
---|
#2527 in Rust patterns
30KB
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])