Skip to content

[Discussion] Reactivity model #114

@cykna

Description

@cykna

Summary

This Issue is intended to discuss about the reactivity model, to make the way things are written more predictable, thus, set rules and expectations of how it should behave like.

Motivation

  • Avoid later conflicts when implementing reactivity model on slynx
  • Help IR and graph resolution phases before fully implemented
  • This is mainly about correctness and architecture.

Current Situation

The current situation of the language is that there's no reactivity yet, even though idealized.
The current state would have problems when working with aliasing, such as the following code:

component Parent {
  prop parentValue = 0;
  Child {
    child_value: parentValue
  }
  Button {
    on_click: _ -> parentValue +=1
  }
}

component Child {
  pub prop childValue: int;
  Button {
    on_click: _ -> childValue += 1;
  }
}

Proposed Direction(s)

The proposed solution for this is to make everything from parent -> child. Thus, we know that 'childValue' in the given example must always be the same as 'parentValue'.
My solution for predictability is to make every component sanboxed, and instead of binding directly via 2 way bind, we simply remove that property if its idealized to be the value of the owner. For example, the given code could become

component Parent {
  prop parentValue = 0;
  Child {
    child_value: parentValue
  }
  Button {
    on_click: _ -> parentValue +=1
  }
}

component Child {
  pub(bind) childValue: int;
  Button {
    on_click: _ -> childValue += 1;
  }
}

The syntax is the same, but now Child when compiling down to, lets say, a C-like struct, would be simply

struct Child {
  btn: Button<F(&int)>
}

Then every usage of 'childValue', instead, we replace it with 'parentValue' and the button modifies directly the real owner of that data. So the 'bind' is like just a reference, but at compile time, so we can get rid of it during runtime.
To separate it properly, any property which isn't marked as 'bind' will be sand-boxed, this means that its mutability is internal, and the parent does not and cannot change it, but only assign its initial value if public.
Suppose:

component Child {
  pub(bind) childValue: int;
  pub anotherRandomValue: int = 0;
  Button {
    on_click: _ -> childValue += 1;
  }
}

'anotherRandomValue' is sandboxed, thus, even though Child {anotherRandomValue: anotherProp} might be able, it will copy the value of anotherProp.

A second approach

Another solution is to make the component mutate only the properties it owns directly. This would imply that a component does not change the values bound from the parent. Then any mutation to 'childValue' following the example above, should instead be an event that is passed to the parent some way.

This could be implemented with an inner queue the parent chooses when to execute, or another method.

Then the child would store in its own queue the events the parent should read, and since the parent owns the child, it can read the queue. But note that the 'queue' example isn't being idealized to be a problem of the front-end, but rather the back-end.
The back-end that should know how to implement this, with a queue, with closures, or any other implementation. The idea then is that this approach is lowered to the IR while keeping the logic
This could then be something like


component Parent {
  prop parentValue = 0;

  func handleChild(event:ChildEvents) {
    switch(event) {
      case .Increment(n): parentValue += n
    }
  }
  Child {
    child_value: parentValue,
    on_event: event -> handleChild(event),
  }
  Button {
    on_click: _ -> parentValue +=1
  }
}

enum ChildEvents {
  Increment(int)
}

component Child {
  type Event = ChildEvents;
  pub(bind) childValue: int;
  pub anotherRandomValue: int = 0;
  Button {
    on_click: _ -> emit Increment(1);
  }
}

Note: type extensions are idealized to be a feature, so 'handleChild' might not be implemented inside the component, but on a extension of it

Open Questions

List the unresolved parts that still need discussion.

  • I haven't thought the downsides yet
  • I haven't idealized properly a way to how pass the references to the ones that might mutate in a cheap way. For now my idea is to use a closure

Extra Context

N/A

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions