vinum-team / Vinum

A modern reactive state management library for correctness and speed.
MIT License
16 stars 1 forks source link

Explicit Constant Dependencies Definitions #12

Closed sinlerdev closed 1 year ago

sinlerdev commented 1 year ago

This is intended as another solution for #3, which solves the issue by allowing the user to define constant dependencies.

Before starting how are we gonna design Explicit definitions for constant dependencies, let's recap the issue we faced that #3 tried to solve.

Consider this pseudocode:

local something = Calc(function(useState)
    return useState(x) and useState(y) or useState(z)
end, AlwaysTrue)

Before diving into the problem this example illustrates, let's divide its behavior into the following:

  1. if both x and y are truthy, z isn't needed to be subscribed to
  2. if any of x and y aren't truthy, z is needed and will be subscribed to

Dependency capturing is done every time an update is performed, which means lazy evaluation is performed so that case 2 is supported, however, an issue arises when we want to support case 1.

The issue is with supported case 1, is dependency recapturing is always performed, providing the same result, which means that most of our computational power is mostly wasted in repeated tasks.

Additionally, this can be a problem with case 2, where x and y are falsy all the time. This means that we have a problem with defining constant dependencies

Explicit Constant Dependencies Definitions is an attempt to solve this issue by allowing the user to explicitly define if specific dependencies are constant or not, which vinum can optimize by marking constant dependencies as objects that don't need to be:

  1. Deactivated at the beginning dependency recapturing
  2. injected by injectors on dependency capturing
  3. simplified at the tree simplification step

At the moment, injector calls are allowed to be called at any point, which that can mean constant dependencies definitions can be ignored by the initial capture. To solve this, we can check if a constant dependency is already injected or not, and if it is not, we can mark it as injected and then do all the subscribing magic, however this can also mean that memory footprint can be increased due to the storing constant dependency references in a different storage. This also means that this feature offers potentially great computational optimization in exchange to an increased memory consumption.


This issue is at the discussion phase, so I ask you to leave all of your feedback here as, at the moment, I have no idea how to approach this issue efficiently, as decreased memory footprint is something I care about as much as decreased computational time.

sinlerdev commented 1 year ago

After thinking a bit more about the "con" of such feature, I think it is avoidable- as instead of relying on a whole new storage for storing constant dependencies, we can already use graph's ._dependencySet.

In addition, after the tree simplification step is finished, we could mark the whole dependent as constant if all of our dependencies are defined as constant- this means that the overhead that comes with dependency deactivation (that still occurs even when all dependencies are constant!), and tree simplification step as well if the dependency capturing still marks the whole dependent as constant.

sinlerdev commented 1 year ago

After some thinking- I believe this on a conceptual level, is the preferred solution for reducing processing time per-update.

Now, I think it is now sensible to design the API that will be used for accessing that internal mechanism- and I have a few API designs:

Syntax Design 1:

Calc(function(useState, useKey)
    return useState/useKey(..., true) -- true means constant
end)

Calc(function(useState, useKey)
    return useState/useKey(..., false) -- false means dynamic
end)

Note: This design doesn't offer no "default" value- preserving explicitness.


Syntax Design 2:

Calc(function(_, _, useConstant)
    return useConstant(obj)/useConstant(obj, key)
end)

Note: While this approach removes the need for passing another argument, it does need yet another overloaded injector


Syntax Design 3:

Calc(function(useState, useKey)
    return useState/useKey(...) -- dependencies are automatically treated as constant 
end)

Calc(function(useState, useKey)
    return useState/useKey(..., false) -- false means dynamic
end)

Note: This is a more of an iteration than Design 1, as it removes the need for manually defining a dependency as constant, however, this optimization will then act as an implicit one, which is against the entire feature's goal.

sinlerdev commented 1 year ago

I think I am going with the second design now that we merged useState and useKeyState into a unified use injector- it signals explicitness very well and avoid some other weird stuff like default values and such.

sinlerdev commented 1 year ago

will be rejected- has figured out a better solution