SodiumFRP / sodium-rust

FRP implementation in Rust
BSD 3-Clause "New" or "Revised" License
72 stars 11 forks source link

Self-modifying Switch #61

Open kevinbaldor opened 3 years ago

kevinbaldor commented 3 years ago

I have attempted to write a sieve of Eratosthenes as a test of self-modifying FRP. The idea is that when fed with a stream of the integers from 2 up, it will emit the series of prime numbers. It works as follows:

Initially, the output stream is defined as equal to the input input -> output Whenever any value arrives at the output, a new stage is defined that takes that value and filters any even multiple of it. So as values are passed in it grows like 2: input -> output: yields 2, adds sieve(2) stage 3: input -> sieve(2) -> output: yields 3, adds sieve(3) stage 4: input -> sieve(2) -> sieve(3) -> output: yields nothing (the value is filtered by sieve(2)) 5: input -> sieve(2) -> sieve(3) -> output: yields 5, adds sieve(5) stage

and so on.

Unfortunately, this isn't working for me. I've attempted to strip it down to the minimal example that demonstrates the issue at https://github.com/kevinbaldor/sodium-rust-test/blob/main/src/main.rs.

I've inserted listeners with println! statements to demonstrate that subscribing to the output works as expected when taking snapshots of normal cells (demonstrated with a constant), but the snapshot of the CellLoop<Stream> that contains the output stream doesn't seem to work, the println! statements don't appear and the Cell::switch_s never changes from its initial value (the input).

It feels like this is something that should be possible. Is this a bug in sodium-rust, or am I using it incorrectly?

clinuxrulz commented 3 years ago

Looks like a bug, I'll have a better look later when I get the time.

clinuxrulz commented 3 years ago

in a transaction the update-wise dependencies need to form a DAG (Directed Acyclic Graph). doing something like let s2 = s1.snapshot(c1, ...). c1 is not considered an update depedency of s2, the only update depedency of s2 is s1 in that example. Since cell's values are updated after end of transaction for view after current transaction they can be snapshoted without being considered an update dependency.

having a look at your sample code it seems that: (let a -> b mean a has an update depedency of b)

output -> output_cell_loop output_cell_loop -> new_output new_output -> output

So the update dependencies does not seem to form a DAG, I think that is where is it falling over (but not 100% sure). We may just need to do your self-modifying switch a different way.

clinuxrulz commented 3 years ago

This is not really exciting, but does the trick:

fn main() {
    let sodium_ctx = SodiumCtx::new();
    let sodium_ctx = &sodium_ctx;
    {
        let ss_input : StreamSink<i64> = sodium_ctx.new_stream_sink();

        let sl_primes: StreamLoop<Vec<i64>> = sodium_ctx.new_stream_loop();
        let s_primes = sl_primes.stream();
        let c_primes = s_primes.hold(Vec::new());
        let s_output =
            ss_input
                .stream()
                .snapshot(
                    &c_primes,
                    |x: &i64, primes: &Vec<i64>| {
                        if primes.iter().any(|prime:&i64|(*x%prime)==0) {
                            None
                        } else {
                            Some(*x)
                        }
                    }
                )
                .filter_option();
        sl_primes.loop_(&s_output.snapshot(&c_primes, |prime:&i64,primes:&Vec<i64>| {
            let mut new_primes = primes.clone();
            new_primes.push(*prime);
            new_primes
        }));

        let l = s_output.listen(|x:&i64| println!("output {}",x));

        for x in 2..20 {
            println!("Sending {}",x);
            ss_input.send(x);
            println!();
        }

        l.unlisten();
    }
}
clinuxrulz commented 3 years ago

Also when switching... for example let s1 = Cell::switch_s(cs), and say cs is changing from an inner stream of s2 to s3 during the same transaction that both s2 and s3 fire. Then s2 will flow through for that transaction and s1 will behave like s3 after that transaction. And I think that is the real reason your seeing all the number instead of just in the prime numbers in the code you've shown.

This switch_s test demonstrates the behaviour of switch.

https://github.com/SodiumFRP/sodium-rust/blob/b00442ceeda6b808302cb9bdfcaffb36198d6413/src/tests.rs#L1306

kevinbaldor commented 3 years ago

I also posted this on the Sodium forum here: http://sodium.nz/t/a-problem-with-a-self-referential-switch/518

It turns out that the Java implementation also has an issue with my original (CellLoop) approach, but it does work when implemented as a StreamLoop.

Unfortunately, the StreamLoop version doesn't work in Rust.

kevinbaldor commented 3 years ago

To address the most recent comment from clinuxrulz, I agree about the semantics of switch. In fact, my code is relying on it. Each output of the switched stream is a prime and I want it to flow through on that transaction (because I want all primes to make it out), but then switched stream changes to reflect the addition of a filter that will block all future multiples of that prime.

clinuxrulz commented 3 years ago

I've posted another workaround here: http://sodium.nz/t/a-problem-with-a-self-referential-switch/518/6?u=clinuxrulz

clinuxrulz commented 3 years ago

I've resolved the bug in version 2.1.1, and Operational::defer must be used to resolve node cycle. See test: https://github.com/SodiumFRP/sodium-rust/blob/dad65115ba3b627b7e9250188789ecfe12905839/src/tests.rs#L1504