Crate bicoro

Crate bicoro 

Source
Expand description

§(Bi)directional (Coro)utines

This is a simple pure library to model a bidirectional coroutine in rust. This allows a coroutine to be defined abstracted away from the actual IO implementations.

The main motivation is writing unit testable coroutines, that respond to inputs, and generate outputs. This is a more generic form of Future.

This is a form of co-operative concurrency. It is also similar to an actor model.

§Mental model

This libraries mental model is ‘pull’ based, similar in concept to rusts iterators, except that we can also ‘ask’ for more input, which is not possible using iterators. This means we don’t have to ‘allocate’ or buffer to much, we only need to box the ‘next thing to do’ as a closure.

There is no borrows used, coroutines become consumed as they are passed around. This is to explictly avoid strange cases of ‘cloning’ a coroutines state, and the complications that would have around how everything is executed.

§Functional

There is one core data type, the coroutine. The rest of the functionality is provided by functions that allow you to glue coroutines and closures together to write your problem.

§Example

use bicoro::*;
use bicoro::iterator::*;
use ::do_notation::m;
 
// The coroutine in dot-notation
let co : Coroutine<i32,String,()> =
        m! {
            value_1 <- receive();
            value_2 <- receive();
            let sum = i32::wrapping_add(value_1,value_2);
            let output = sum.to_string();
            send(output);
            result(())
        };
 
// Arrange inputs and an store outputs in a vec
let inputs = vec![1,2];

// creates an interator of outputs, fed via the inputs
let mut it = as_iterator(co,inputs.into_iter());

// By ref so we don't consume the iterator (important if
// we want to get the remaining inputs or coroutine)
let outputs = it.by_ref().collect::<Vec<_>>();
// Verify output values
assert_eq!(outputs, vec!["3"]); 

// Consume the iterator, so that we can check results
let (result,mut remaining_inputs) = it.finish();
// We return the result from the coroutine.
assert!(matches!(result,Result::Ok(())));
// All the inputs from the iterator were consumed
assert!(matches!(remaining_inputs.unwrap().next(),None));

§Executing

The coroutine just describes what to do, it won’t actually do anything. You need to run it through an executor. One that works on an iterator is provided, but you may also want to create your own to implement with different contexts.

§Use cases

§Protocol descriptions

This can be used to describe protocols using the sans-io philosophy https://sans-io.readthedocs.io. A protocol can be thought of an actor that recieves input messages and responds with outputs, maintaing it’s own internal state. As the coroutines are nestable and composeable, we can create subroutines and functions to solve particular ‘sub states’ of the protocol, e.g connecting then transitioning to the main part of the protocol.

An executor can then map to low level IO functions, e.g read/write bytes. Having these seperate means the protocol implementation can be thoroughly tested

§State Machines

We can model state machines (see examples). Particularly useful as we can compose and combine state machines like in protocols. Types can even show that there is no ‘final’ state for the machine e.g Coroutine<Input,Ouput,!> implies we can always feed information to the machine.

On state transitions, we can also emit multiple messages or none, so we are flexible on what the output is.

§Iterator with a terminating state

Coroutine<!,Output,Result> is much like a rust iterator, except that the final result doesn’t need to be the same type. This can be useful as you don’t need to create an awkward enum, and only ever create the ‘termination’ state at the end. It also more clearly communicates ‘we are actually done now’ and consumes the routine, so it can’t be re-used accidentally.

§Folds

Coroutine<Input,!,Result> is similar to folding (reduce) a set of inputs. This can be written without even having an iterator. E.g it could be in a loop where inputs are read as needed, which is handy for interactive components.

§do-notatation

Implementation is compatible with https://github.com/phaazon/do-notation allowing for a syntax ‘similar in feel’ to using .await on futures.

This helps a bit with the callback hell, but is not strictly necessary.

Modules§

executor
A sample executor that consumes an iterator
iterator
Convert the coroutine to an iterator

Structs§

Coroutine
A structure describing a co-routine supporting sends (inputs), yields (outputs), and a final termination (result)

Enums§

ChainResult
Generated by chain, represents feeding inputs from one routine into another
CooperateResult
Represents the result of running the left and right coroutines with the ability to share inputs to each other
DispatchResult
Represents the result of running the left and right coroutines Returns whichever coroutine finished first
ObserveResult
RoutedResult
Select
A selection for which coroutine to route to
StepResult
A step wise evalution of the coroutine
UnicastSelect
A more specific version of select, where messages are exclusive

Functions§

bind
Chain outputs together.
bind_err
Process the err value, or continue with ok
bind_ok
Process the Ok value, or short-circuit
bind_some
Process the Some value, or short-circuit
broadcast
Sends inputs to both coroutines, and will emit outputs together
broadcast_until_finished
Sends inputs to both coroutines, and will emit outputs together
chain
Feeds the outputs of the first routine into the inputs of the second
cooperate
dispatch
Run two co-routines, sharing inputs depending on selector.
inject
Use this input for the next input
intercept_input
Transforms the input of coroutine A into B
intercept_output
Transforms the output of coroutine A into B
left
Runs a routine before the second routine
map
Map the inner type of the coroutine
map_err
Convert E1 into E2
map_input
Takes a coroutine that wants a, and ‘lifts’ or maps it into B
map_output
Takes a coutine outputting A, and makes it output B
map_some
Maps the inner value
observe
receive
Suspend this coroutine until an input arrives
receive_or_skip
Use to either consume this input or re-emit as an output
recieve_until
Runs recieve until f returns some
result
Return/unit. Creates a result of the supplied value
right
Runs a routine before the second routine
routed
Chain and dispatch combined.
run_step
Runs a single step in the coroutine.
send
Yields a value to the executor
subroutine
Runs a subroutine and converts it to the hosted type
subroutine_result
Run a child subroutine, with routines that map inputs and outputs
suspend
Suspend this coroutine until an input arrives with a function
try_observe
Get the next output of the coroutine
tuple
Runs two coroutines sequentially
unicast
Run two co-routines, sharing inputs depending on selector.
unicast_until_finished
Unicast until both routines are completed
void
Converts the return result to the unit type

Type Aliases§

BroadcastRoutine