RibirX / Ribir

Non-intrusive GUI framework for Rust

Home Page:https://ribir.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Validity of the SplittedWriter

wjian23 opened this issue · comments

Validity of the SplittedWriter

intention

The original intention of this design is to deal with the case when the data split_writer is referenced from the parent, but the data is change from parent writer, which unexpected to the SplittedWriter.

  1. The data ref by SplittedWriter may be destroyed or changed, which we can't continue to handle with user's operations.
  struct App {
    data: Option<i32>,
  }

  fn compose_from_data(data: SplittedWriter<i32>) {
    // keep the data and do something after
  }

  fn example() {
    let app = State::new(App { data: Some(1) });
    let data = app.split_writer(
      |app| app.data.as_ref().unwrap(),
      |app| &mut app.data.as_mut().unwrap()
    );

    compose_from_data(data);
    app.data = None;
  }

after the app.data = None, the data is drop, the use of data in compose_from_data is invalid.

  1. If the SplittedWriter is mapped with an index, it is not stable when the collection changes.
  struct App {
    data: Vec<i32>,
  }

  fn compose_from_data(data: SplittedWriter<Vec<i32>>) {
    // keep the data and do something after
    fn_widget! {
      pipe! {
        data.len().map(|idx| {
          @ DataWidget {
            xxx
            on_click: || {
              // modify data[idx]
            }
          }
        })
      }
    }
  }

  fn example() {
    let app = State::new(App { data: vec![] });
    let data = app.split_writer(
      |app| &app.data,
      |app| &mut app.data
    );

    compose_from_data(data);
    app.data.insert(0, 1);
  }

when the app.data.insert(0, 1) is called, the data the widget and the data is no longer the same, the modification of the data may be Unexpected.

As we can see is easy to make bugs, so we ban by panic the use of split_writer after origin_writer changed.

discussion

However this design is not perfect, if the SplittedWriter is referenced to data that is modified only by itself, then the SplittedWriter should never outdated when parent changed.
The key point is we don't know whether the SplittedWriter is valid or not when it's parent changed, it should be decided by user.

Api changed

The split_writer will export api to set checker to test whether the SplittedWriter is valid or not, and panic should happen when use of outdated writer.

pub trait SplitChecker<P: ?Sized> {
  fn is_valid<S: StateReader<Value = P>>(&self, src: &S) -> bool;
}
  trait SplittedWriter {
    ...
    // set the checker, the writer use the checker to verify whether it is valid or not.
    set_checker(&mut self, checker: impl SplitChecker<Parent = Self::OriginWriter::Value> + 'static);
  }

  trait StateReader {
    ...
    // if the StateReader is unstable, the user can check it before used.
    fn is_valid(&self) -> bool;
  }

Check of OriginWriter

the detail of the invalid check also need to be discussed.
the checker should be invalid if the origin_writer is invalid. but how to pass the invalid state from origin_writer to the split_writer?

  1. we can call origin_writer.is_valid() recursive to check every time before used, it's simple and reliable, but all the origin from self to the source root check will be checked.

  2. when the origin_writer is outdated, the child split_writer will be invoke to invalid and the state must be changed before the next round of the subscribe be called. the most closest time for the framework to check the state is when the WriteRef return from writer's write() is drop.However, this implement will be more complex

commented

Discussion Conclusion:

The split writer/reader no longer automatically becomes invalid but is judged by the result of the split to imply whether the split Reader/Writer is valid.

Two new traits are added:

trait ReadSplitter {
  type Output<'a>;
  fn split(&self) -> Self::Output;
}

trait WriteSplitter {
  type Output<'a>;
  fn split_mut(&mut self) -> Self::Output;
}

By default

  • FnMut(&T) -> &R,
  • FnMut(&T) -> Option<&R>
  • FnMut<E>(&T) -> Result<&R, E>

implement ReadSplitter.

  • FnMut(&mut T) -> &mut R
  • FnMut(&mut T) -> Option<&mut R>
  • FnMut<E>(&mut T) -> Result<&mut R, E>

implement WriteSplitter.

Users can also implement these two traits according to their own needs.

commented

A simple demo show how to split the reader of a state.

use std::{
  cell::{Ref, RefCell},
  io::Split,
  ops::Deref,
};

pub trait ReadSplitter<'a, In> {
  type Out<'r>
  where
    'a: 'r,
    In: 'a;
  fn split(&self, input: &'a In) -> Self::Out<'a>;
}

#[derive(Clone, Copy)]
struct Splitter<F>(F);

impl<'a, F, In: 'a, Out: 'a> ReadSplitter<'a, In> for Splitter<F>
where
  F: Fn(&In) -> &Out,
{
  type Out<'r> = &'r Out where 'a: 'r, In: 'a;
  fn split(&self, input: &'a In) -> Self::Out<'a> { (self.0)(input) }
}

pub fn splitter<'a, In: 'a, Out: 'a>(
  f: impl Fn(&In) -> &Out + Copy,
) -> impl ReadSplitter<'a, In, Out<'a> = &'a Out> + Copy {
  Splitter(f)
}

#[derive(Clone, Copy)]
struct OptionSplitter<F>(F);

impl<'a, F, In: 'a, Out: 'a> ReadSplitter<'a, In> for OptionSplitter<F>
where
  F: Fn(&In) -> Option<&Out>,
{
  type Out<'r> = Option<&'r Out> where 'a:'r;
  fn split(&self, input: &'a In) -> Self::Out<'a> { (self.0)(input) }
}

pub fn option_splitter<'a, In: 'a, Out: 'a>(
  f: impl Fn(&In) -> Option<&Out> + Copy,
) -> impl ReadSplitter<'a, In, Out<'a> = Option<&'a Out>> + Copy {
  OptionSplitter(f)
}

// ResultSplitter can be implemented in the same way as OptionSplitter

struct Stateful<W>(W);

struct MapReader<O, S> {
  origin: O,
  splitter: S,
}

trait StateReader<'a> {
  type Value;

  fn map_reader<'r>(
    &self,
    s: impl ReadSplitter<'r, Self::Value>,
  ) -> MapReader<&Self, impl ReadSplitter<'r, Self::Value>> {
    MapReader { origin: self, splitter: s }
  }
}

impl<'a, W> StateReader<'a> for Stateful<W> {
  type Value = W;
}

impl<'a, O, S> StateReader<'a> for MapReader<O, S>
where
  O: StateReader<'a>,
  O::Value: 'a,
  S: ReadSplitter<'a, O::Value>,
{
  type Value = S::Out<'a>;
}

fn main() {
  let w = Stateful(30);
  let r = w.map_reader(splitter(|w| w));
  let option = Stateful(Some(30));
  let r = option.map_reader(option_splitter(|v: &Option<i32>| v.as_ref().map(|v| v)));
}
commented

A simple demo show how to split the reader of a state.

use std::{
  cell::{Ref, RefCell},
  io::Split,
  ops::Deref,
};

pub trait ReadSplitter<'a, In> {
  type Out<'r>
  where
    'a: 'r,
    In: 'a;
  fn split(&self, input: &'a In) -> Self::Out<'a>;
}

#[derive(Clone, Copy)]
struct Splitter<F>(F);

impl<'a, F, In: 'a, Out: 'a> ReadSplitter<'a, In> for Splitter<F>
where
  F: Fn(&In) -> &Out,
{
  type Out<'r> = &'r Out where 'a: 'r, In: 'a;
  fn split(&self, input: &'a In) -> Self::Out<'a> { (self.0)(input) }
}

pub fn splitter<'a, In: 'a, Out: 'a>(
  f: impl Fn(&In) -> &Out + Copy,
) -> impl ReadSplitter<'a, In, Out<'a> = &'a Out> + Copy {
  Splitter(f)
}

#[derive(Clone, Copy)]
struct OptionSplitter<F>(F);

impl<'a, F, In: 'a, Out: 'a> ReadSplitter<'a, In> for OptionSplitter<F>
where
  F: Fn(&In) -> Option<&Out>,
{
  type Out<'r> = Option<&'r Out> where 'a:'r;
  fn split(&self, input: &'a In) -> Self::Out<'a> { (self.0)(input) }
}

pub fn option_splitter<'a, In: 'a, Out: 'a>(
  f: impl Fn(&In) -> Option<&Out> + Copy,
) -> impl ReadSplitter<'a, In, Out<'a> = Option<&'a Out>> + Copy {
  OptionSplitter(f)
}

// ResultSplitter can be implemented in the same way as OptionSplitter

struct Stateful<W>(W);

struct MapReader<O, S> {
  origin: O,
  splitter: S,
}

trait StateReader<'a> {
  type Value;

  fn map_reader<'r>(
    &self,
    s: impl ReadSplitter<'r, Self::Value>,
  ) -> MapReader<&Self, impl ReadSplitter<'r, Self::Value>> {
    MapReader { origin: self, splitter: s }
  }
}

impl<'a, W> StateReader<'a> for Stateful<W> {
  type Value = W;
}

impl<'a, O, S> StateReader<'a> for MapReader<O, S>
where
  O: StateReader<'a>,
  O::Value: 'a,
  S: ReadSplitter<'a, O::Value>,
{
  type Value = S::Out<'a>;
}

fn main() {
  let w = Stateful(30);
  let r = w.map_reader(splitter(|w| w));
  let option = Stateful(Some(30));
  let r = option.map_reader(option_splitter(|v: &Option<i32>| v.as_ref().map(|v| v)));
}

The current implementation, which uses GAT, is complex and verbose. This leads to intricate lifetime issues and cryptic error messages when compilation fails. A more straightforward implementation, as suggested below, could be considered.

use std::{
  cell::{Ref, RefCell},
  io::Split,
  ops::Deref,
};

struct Stateful<W>(W);
struct MapReader<O, S> {
  origin: O,
  splitter: S,
}

trait StateReader<'a> {
  type Value;

  fn value(&self) -> &Self::Value { todo!() }

  fn map_reader<S>(self, s: S) -> MapReader<Self, S>
  where
    Self: Sized,
    S: ReadSplitter,
  {
    MapReader { origin: self, splitter: s }
  }
}

impl<'a, W> StateReader<'a> for Stateful<W> {
  type Value = W;
}

#[derive(Clone, Copy)]
pub struct Splitter<F>(F);

impl<F> Splitter<F> {
  pub fn read<In, Out>(f: F) -> Self
  where
    F: Fn(&In) -> &Out + Copy,
  {
    Self(f)
  }
}

impl<'a, O, F, V> StateReader<'a> for MapReader<O, Splitter<F>>
where
  O: StateReader<'a>,
  F: Fn(&O::Value) -> &V + Copy,
{
  type Value = V;
}

#[derive(Clone, Copy)]
pub struct ToOptionSplitter<F>(F);

impl<F> ToOptionSplitter<F> {
  pub fn read<In, Out>(f: F) -> Self
  where
    F: Fn(&In) -> Option<&Out> + Copy,
  {
    Self(f)
  }
}

impl<'a, O, F, Val: 'a> StateReader<'a> for MapReader<O, ToOptionSplitter<F>>
where
  O: StateReader<'a>,
  F: Fn(&O::Value) -> Option<&Val> + Copy,
{
  type Value = Option<&'a Val>;
}

pub trait ReadSplitter {}

impl<F> ReadSplitter for Splitter<F> {}
impl<F> ReadSplitter for ToOptionSplitter<F> {}

fn main() {
  let w = Stateful(30);
  let r = w.map_reader(Splitter::read(|w: &i32| w));
  let option = Stateful(Some(30));

  let r = option
    .map_reader(ToOptionSplitter::read(|v: &Option<i32>| v.as_ref()))
    .map_reader(ToOptionSplitter::read(|v: &Option<&i32>| v.map(|v| v)));
}