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

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

Structs

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

Enums

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

Functions

Chain outputs together.
Process the err value, or continue with ok
Process the Ok value, or short-circuit
Process the Some value, or short-circuit
Sends inputs to both coroutines, and will emit outputs together
Sends inputs to both coroutines, and will emit outputs together
Feeds the outputs of the first routine into the inputs of the second
Run two co-routines, sharing inputs depending on selector.
Use this input for the next input
Transforms the input of coroutine A into B
Transforms the output of coroutine A into B
Runs a routine before the second routine
Map the inner type of the coroutine
Convert E1 into E2
Takes a coroutine that wants a, and ‘lifts’ or maps it into B
Takes a coutine outputting A, and makes it output B
Maps the inner value
Suspend this coroutine until an input arrives
Use to either consume this input or re-emit as an output
Runs recieve until f returns some
Return/unit. Creates a result of the supplied value
Runs a routine before the second routine
Chain and dispatch combined.
Runs a single step in the coroutine.
Yields a value to the executor
Runs a subroutine and converts it to the hosted type
Run a child subroutine, with routines that map inputs and outputs
Suspend this coroutine until an input arrives with a function
Get the next output of the coroutine
Runs two coroutines sequentially
Run two co-routines, sharing inputs depending on selector.
Unicast until both routines are completed
Converts the return result to the unit type

Type Definitions