Crate rspl

source ·
Expand description

rspl is a stream-processor language based on the one discussed in Generalising Monads to Arrows using Rust as meta-language.

Design

Essentially, rspl is a way to encode functions from streams to streams such that control is syntactically explicit (like in ordinary continuation-passing style) refining the orthodox functional approach to stream processing with combinators like ‘map’. More precisely, the idea of this stream-processor language is to split the processing of streams into two parts: One part for reading (getting) the first element of an input stream to direct the further processing. Another part for writing (putting) something to the output stream and offering to process some input stream if needed. Combining these parts in various ways allows to flexibly construct stream processors as programs. The following graphic illustrates how those two different kinds of stream processors (‘getting’ and ‘putting’) work (whereas a textual description is contained in the docs of StreamProcessor):

h--t1--t2--t3--...                   ha--t1--t2--t3--...
-                                    -
|                                    |
| Get(h |-> [SP](h))                 | Put(hb, LAZY-[SP])
|                                    |
v                                    |
t1--t2--t3--...                      |   t1--t2--t3--...
-                                    |   -
|                                    v   |
| [SP](h) = Get(_)                   hb--| LAZY-[SP]() = Get(_)
|                                        |
v                                        v
...                                      ...


h--t1--t2--t3--...                   ha--t1--t2--t3--...
-                                    -
|                                    |
| Get(h |-> [SP](h))                 | Put(hb, LAZY-[SP])
|                                    |
v                                    |
h--t1--t2--t3--...                   |   ha--t1--t2--t3--...
-                                    |   -
|                                    v   |
| [SP](h) = Put(_, _)                hb--| LAZY-[SP]() = Put(_, _)
|                                        |
v                                        v
...                                      ...

Remarkably, the language constructs are somewhat dual and loosely correspond to (dual) programming paradigms:

  • The Get-construct relates to event-driven programming as it reacts to input (events) eagerly.
  • The Put-construct relates to demand-driven1 programming as it generates output (demands) iteratively by need.

This will be discussed further in the Examples-section.

Usage

To program a rspl-StreamProcessor you just have to compose the constructors StreamProcessor::Get/get and StreamProcessor::Put/put in the right way. For a somewhat more high-level programming experience you might wish to look at the combinators-module. The program can then be evaluated with the eval-method on some kind of input stream. The ‘kind’ of input stream is either your own implementation of the Stream-interface or one from the submodules of the streams-module. Either way, as result, evaluation produces an InfiniteList (lazily). To observe streams - and i.p. infinite lists - you can destruct them with the head- and tail-methods of the stream interface. Moreover, there are various functions helping with the destruction and construction of streams.

Examples

As alluded to in the Design-section, rspl supports orthodox ‘combinator-driven’ stream processing as it is known from list processing with combinators like compose, filter and map. For example, it is possible to first filter some ‘bad’ elements out of a stream in order to safely iterate some function over the resulting stream afterwards in a combinatorial way. Such a usage of rspl looks like:

use rspl::combinators::{compose, filter, map};
use rspl::streams::infinite_lists::InfiniteList;
use rspl::streams::Stream;
use rspl::StreamProcessor;

let is_greater_zero = |n: &usize| *n > 0;
let minus_one = |n: usize| n - 1;

let zeroes = compose(filter(is_greater_zero), map(minus_one))
    .eval(InfiniteList::cons(0, || InfiniteList::constant(1)));

assert_eq!(*zeroes.head(), 0);

More interestingly, rspl can also serve as a framework for the nifty idea of

  • event-driven programming with state machines as suggested here. Abstractly, that usage of rspl looks as follows:

    use rspl::streams::infinite_lists::InfiniteList;
    use rspl::streams::Stream;
    use rspl::StreamProcessor;
    
    #[derive(Copy, Clone)]
    enum Event {
        Event1,
        Event2,
    }
    
    fn action() -> bool {
        true
    }
    
    fn state_1<'a>() -> StreamProcessor<'a, Event, bool> {
        fn transition<'a>(event: Event) -> StreamProcessor<'a, Event, bool> {
            match event {
                Event::Event1 => StreamProcessor::put(action(), state_1),
                Event::Event2 => state_2(),
            }
        }
    
        StreamProcessor::get(transition)
    }
    
    fn state_2<'a>() -> StreamProcessor<'a, Event, bool> {
        fn transition<'a>(event: Event) -> StreamProcessor<'a, Event, bool> {
            match event {
                Event::Event1 => state_1(),
                Event::Event2 => StreamProcessor::put(false, state_2),
            }
        }
    
        StreamProcessor::get(transition)
    }
    
    let event_loop = state_2().eval(InfiniteList::constant(Event::Event1));
    
    assert!(event_loop.head());

    A slightly more concrete example using that pattern is available as integration test. And a full-blown concrete example of a pelican crossing can be found here (as .md file) and here (as .rs file). Notably, it uses rspl to encode effectful hierarchical state machines with a capability-passing inspired effect-handling mechanism.

  • demand-driven programming with generators as suggested here. Abstractly, that usage of rspl looks as follows:

    use rspl::streams::infinite_lists::InfiniteList;
    use rspl::streams::Stream;
    use rspl::StreamProcessor;
    
    struct State {
        toggle: bool,
    }
    
    fn action(state: &mut State) {
        state.toggle = !state.toggle;
    }
    
    fn pre_action(state: State) -> State {
        state
    }
    
    fn post_action(state: State) -> State {
        state
    }
    
    fn generator_name<'a>(mut state: State) -> StreamProcessor<'a, (), bool> {
        state = pre_action(state);
        StreamProcessor::get(|_| {
            action(&mut state);
            StreamProcessor::put(state.toggle, || generator_name(post_action(state)))
        })
    }
    
    let generations = generator_name(State { toggle: false }).eval(InfiniteList::constant(()));
    
    assert!(generations.head());

    A slightly more concrete example using that pattern is available as integration test. And a full-blown concrete example of a heat index control system can be found here (as .md file) and here (as .rs file).


  1. Look at Codata in Action for some more explanation on that term. 

Modules

  • This module defines functions which combine existing stream processors into new ones. In particular, there are nullary combinators to get writing a stream processor off the ground.
  • This module defines streams of some type intensionally by means of a trait. Additionally, the module declares submodules with implementations of the trait.

Enums

  • StreamProcessor<A, B> defines (the syntax of) a language describing the domain of stream processors, that is, terms which can be interpreted to turn streams of type A into streams of type B.