//&# rspl Heapless //& //&This example explores the viability of an alternative implementation of rspl based on something like closure conversion to avoid the use of a heap. //&Such an implementation would make rspl better suited for embedded systems as it could not only forgo the standard library but also an allocator. //&In the rest of this file we explain and implement that heapless approach to draw a tentative conclusion on its viability. //&rspl uses the heap essentially for laziness when it thunks stream processing and tails of streams. //&This is because in those cases Rust does not statically determine the size of the thunks. //&Assisting Rust in that regard by converting those closures eliminates the need for a heap.\ //&What we mean by converting closures here can be understood by looking at the example of streams as codata[^1]. //&In Agda one defines streams of type `X` (intensionally) by the codata type //&```agda //&record StreamX : Set where //& coinductive //& field //& head : X //& tail : StreamX //&``` //&This can be read as 'any object from which one can observe something of type `X` (the `head`) and a something of type `StreamX` (the `tail`) is a `StreamX`'. //&Defining a stream of `X`s can then be done by copatttern matching, that is, by specifying what the `head` and the `tail` of the stream shall be. //&For example, given some `x : X` one can construct the constant stream of `x` by the comatch //&```agda //&constant : X -> StreamX //&head (constant x) = x //&tail (constant x) = constant x //&``` //&Now, to encode `constant` in Rust one can convert that generalized closure by making its environment (its domain) explicit by means of a struct (as usual), and implement the `Stream`-trait accordingly: //&```rust trait Stream { fn head(&self) -> &X; fn tail(self) -> Self; } struct ComatchConstant { constant: X, } impl Stream for ComatchConstant { fn head(&self) -> &X { &self.constant } fn tail(self) -> Self { self } } //&``` //&This also works in greater generality for mutually recursive codata. //&For ordinary stream processors in Agda //&```agda //&data StreamProcessor : Set where //& get : (A -> StreamProcessor) -> StreamProcessor //& put : B -> LazySP -> StreamProcessor //& //&record LazySP where //& coinductive //& field //& force : StreamProcessor //&``` //&we get the following rspl equivalent (if we also generalize `X -> Y` to the intensional definition of function with the help of codata) in Rust: //&```rust enum StreamProcessor where F: Fun, L: LazySP, { Get(F, std::marker::PhantomData), Put(B, L), } trait Fun where F: Fun, L: LazySP, { fn apply(self, a: A) -> StreamProcessor; } trait LazySP where F: Fun, L: LazySP, { fn force(self) -> StreamProcessor; } struct ComatchEval where S: Stream, F: Fun, L: LazySP, { phantom_a: std::marker::PhantomData, phantom_f: std::marker::PhantomData, stream: S, output: B, lazy_sp: L, } impl Stream for ComatchEval where A: Clone, S: Stream, F: Fun, L: LazySP, { fn head(&self) -> &B { &self.output } fn tail(mut self) -> Self { let sp = self.lazy_sp.force(); if let StreamProcessor::Get(_, _) = sp { self.stream = self.stream.tail(); } StreamProcessor::eval(sp, self.stream) } } impl StreamProcessor where F: Fun, L: LazySP, { fn eval>(mut self, mut stream: S) -> ComatchEval where A: Clone, { loop { match self { StreamProcessor::Get(f, _) => { self = f.apply(stream.head().clone()); while let StreamProcessor::Get(f, _) = self { stream = stream.tail(); self = f.apply(stream.head().clone()); } continue; } StreamProcessor::Put(b, lazy_sp) => { return ComatchEval { phantom_a: std::marker::PhantomData, phantom_f: std::marker::PhantomData, stream, output: b, lazy_sp, } } } } } } //&``` //&Then, for a proof of concept we can reimplement the `map`-combinator and apply it to some stream: //&```rust struct ComatchMap<'a, A: 'a, B: 'a, F> where F: Fn(A) -> B, { a: std::marker::PhantomData<&'a A>, b: std::marker::PhantomData<&'a B>, function: F, } type SPMap<'a, A, B, F> = StreamProcessor, ComatchMap<'a, A, B, F>>; impl<'a, A, B, F> Fun, ComatchMap<'a, A, B, F>> for ComatchMap<'a, A, B, F> where F: Fn(A) -> B, { fn apply(self, a: A) -> SPMap<'a, A, B, F> { StreamProcessor::Put((self.function)(a), self) } } impl<'a, A, B, F> LazySP, ComatchMap<'a, A, B, F>> for ComatchMap<'a, A, B, F> where F: Fn(A) -> B, { fn force(self) -> SPMap<'a, A, B, F> { map(self.function) } } const fn map<'a, A: 'a, B: 'a, F>(f: F) -> SPMap<'a, A, B, F> where F: Fn(A) -> B, { StreamProcessor::Get( ComatchMap { a: std::marker::PhantomData, b: std::marker::PhantomData, function: f, }, std::marker::PhantomData, ) } fn main() { let trues = ComatchConstant { constant: true }; let mut stream = map(|b: bool| !b).eval(trues); for _ in 0..1_000_000 { println!("{}", stream.head()); stream = stream.tail(); } } //&``` //&The tentative conclusion is that while the approach seems doable, it has significant negative consequences: stream processors become harder to understand and more tedious to write. //&Therefore, the approach is impractical. //&At least, without a better language frontend. //&[^1]: A modern take on OOP generalizing closures (see e.g. [Codata in Action](https://www.microsoft.com/en-us/research/uploads/prod/2020/01/CoDataInAction.pdf)).