This commit is contained in:
aaron 2023-08-06 23:16:58 +01:00
parent e03082c941
commit fa2709f182
8 changed files with 367 additions and 216 deletions

View file

@ -32,100 +32,6 @@ Creates a new source state with the given value.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
## derive()
Derives a new state from existing states.
- ### Type
```lua
function derive<T>(source: () -> T): () -> T
```
- ### Details
The derived state will have its value recalculated when any source state it
derives from is updated.
Anytime its value is recalculated it is also cached, subsequent calls will
retun this cached value until it recalculates again.
Takes a callback that is immediately run to determine what states are being
referenced.
> ⚠️ Non-yielding.
- ### Example
```lua
local count = wrap(0)
local text = derive(function() return `count: {count()}` end)
text() -- "count: 0"
count(1)
text() -- "count: 1"
```
--------------------------------------------------------------------------------
## map()
Maps each value in a table state to a new table state.
- ### Type
```lua
function map<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Map<KI, VO>
- ### Details
The transform function is called only ever *once* for each index in the
source table. The first argument is a state containing the index's value and
the second argument is just the index.
Anytime a new index is added, the transform function will be called again for
that new index.
Anytime an existing index changes, the transform function is not rerun,
instead the passed state for that index will update, causing anything
depending on it to update too.
Returns a state containing the mapped key-value pairs.
> ⚠️ Non-yielding.
- ### Example
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = map(numbers, function(item, i)
return ItemDisplay {
Name = function()
return item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
LayoutOrder = i
}
end)
```
--------------------------------------------------------------------------------
## watch() ## watch()
Runs a callback on state change. Runs a callback on state change.
@ -166,3 +72,179 @@ Runs a callback on state change.
``` ```
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
## derive()
Derives a new state from existing states.
- ### Type
```lua
function derive<T>(source: () -> T): () -> T
```
- ### Details
The derived state will have its value recalculated when any source state it
derives from is updated.
Anytime its value is recalculated it is also cached, subsequent calls will
retun this cached value until it recalculates again.
Takes a callback that is immediately run to determine what states are being
referenced.
> ⚠️ Non-yielding.
- ### Example
```lua
local count = wrap(0)
local text = derive(function() return `count: {count()}` end)
text() -- "count: 0"
count(1)
text() -- "count: 1"
```
--------------------------------------------------------------------------------
## indexes()
Maps each index in a table to an object.
- ### Type
```lua
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Array<VO>
- ### Details
The transform function is called only ever *once* for each index in the
source table. The first argument is a state containing the index's value and
the second argument is just the index.
Anytime a new index is added, the transform function will be called again for
that new index.
Anytime an existing index value changes, the transform function is not rerun,
instead the passed state for that index will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the transform.
> ⚠️ Non-yielding.
- ### Example
The intended purpose of this function is to map each index in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(numbers, function(item, i)
return ItemDisplay {
Name = function()
return item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
LayoutOrder = i
}
end)
```
--------------------------------------------------------------------------------
## values()
Maps each value in a table to an object.
- ### Type
```lua
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: VI, index: () -> KI) -> VO
): Array<VO>
- ### Details
The transform function is called only ever *once* for each value in the
source table. The first argument is the index's value and
the second argument is a state containing the index.
Anytime a new value is added, the transform function will be called again
for that new value.
Anytime an existing value's index changes, the transform function is not
rerun, instead the passed state for that value will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the transform.
> ⚠️ Non-yielding.
- ### Example
The intended purpose of this function is to map each value in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(numbers, function(item, i)
return ItemDisplay {
Name = item.Name
Image = "rbxassetid://" .. item.icon,
LayoutOrder = i
}
end)
```
- ### Extra
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- Toast notifications.
`indexes()` should be used in other cases, especially when your source table
has primitive value. It maps an index to a UI element.
e.g.
- List of character or weapon stats.
In most cases, both functions will appear to have the same behavior.
The main difference is performance, picking the right function to use can
result in less property updates and less re-renders.
--------------------------------------------------------------------------------

View file

@ -13,7 +13,7 @@ type Item = {
local items = source({} :: Array<Item>) local items = source({} :: Array<Item>)
List { List {
Children = map(items, function(item, i) Children = indexes(items, function(item, i)
return create "ImageLabel" { return create "ImageLabel" {
Image = function() Image = function()
return "rbxassetid://" .. item().Icon return "rbxassetid://" .. item().Icon

View file

@ -88,32 +88,32 @@ local function bind_parent(instance: Instance, fn: () -> Instance?)
end end
local function bind_children(parent: Instance, fn: () -> { Instance }) local function bind_children(parent: Instance, fn: () -> { Instance })
local currentChildrenSet: { [Instance]: true } = {} -- cache of all children parented before update local current_child_set: { [Instance]: true } = {} -- cache of all children parented before update
local newChildrenSet: { [Instance]: true } = {} -- cache of all children parented after update local new_child_set: { [Instance]: true } = {} -- cache of all children parented after update
setup(parent, function(parent_weak) setup(parent, function(parent_weak)
local newChildren = fn() -- all (and only) children that should be parented after this update local new_childs = fn() -- all (and only) children that should be parented after this update
if newChildren and type(newChildren) ~= "table" then if new_childs and type(new_childs) ~= "table" then
throw(`Cannot parent instance of type { type(newChildren) } `) throw(`Cannot parent instance of type { type(new_childs) } `)
end end
if newChildren then if new_childs then
for _, child in next, newChildren do for _, child in next, new_childs do
newChildrenSet[child] = true -- record child set from this update new_child_set[child] = true -- record child set from this update
if not currentChildrenSet[child] then if not current_child_set[child] then
child.Parent = parent_weak -- if child wasn't already parented then parent it child.Parent = parent_weak -- if child wasn't already parented then parent it
else else
currentChildrenSet[child] = nil -- remove child from cache if it was already in cache current_child_set[child] = nil -- remove child from cache if it was already in cache
end end
end end
end end
for child in next, currentChildrenSet do for child in next, current_child_set do
child.Parent = nil -- unparent all children that weren't in the new children set child.Parent = nil -- unparent all children that weren't in the new children set
end end
table.clear(currentChildrenSet) -- clear cache, preserve capacity table.clear(current_child_set) -- clear cache, preserve capacity
currentChildrenSet, newChildrenSet = newChildrenSet, currentChildrenSet current_child_set, new_child_set = new_child_set, current_child_set
end) end)
end end

View file

@ -1,65 +0,0 @@
if not game then script = (require :: any) "test/wrap-require" end
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local create = graph.create
local set = graph.set
local capture = graph.capture
local link = graph.link
type Map<K, V> = { [K]: V }
local function map<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> Map<K, VO>
local input_cache = {} :: Map<K, VI>
local output_cache = {} :: Map<K, VO>
local input_nodes = {} :: Map<K, Node<VI>>
local remove_queue = {} :: { K }
local function recompute(data)
-- queue removed values
for k in next, input_cache do
if data[k] == nil then
table.insert(remove_queue, k)
end
end
-- remove queued values
for _, k in next, remove_queue do
input_cache[k] = nil
output_cache[k] = nil
input_nodes[k] = nil
end
-- process new or changed values
for k, v in next, data do
if input_cache[k] == nil then
local node, get_value = create(v)
input_nodes[k] = node
output_cache[k] = transform(get_value, k)
elseif input_cache[k] ~= v then
set(input_nodes[k], v)
end
input_cache[k] = v
end
return output_cache
end
local function derive()
return recompute(input())
end
local output, output_get = create(output_cache)
local nodes, value = capture(input)
for _, node in next, nodes do
link(node, output, derive)
end
output.cache = recompute(value)
return output_get
end
return map

View file

@ -9,13 +9,13 @@ local link = graph.link
type Map<K, V> = { [K]: V } type Map<K, V> = { [K]: V }
-- todo: optimize, double buffering? -- todo: this could be optimized
local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO } local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI, K) -> VO): () -> { VO }
local input_cache = {} :: Map<K, VI> local input_cache = {} :: Map<K, VI>
local output_cache = {} :: Map<K, VO> local output_cache = {} :: Map<K, VO>
local input_nodes = {} :: Map<K, Node<VI>> local input_nodes = {} :: Map<K, Node<VI>>
local remove_queue = {} :: { K } local remove_queue = {} :: { K }
local output_array = {} :: { VO }
local function recompute(data) local function recompute(data)
-- queue removed values -- queue removed values
@ -34,23 +34,25 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
-- process new or changed values -- process new or changed values
for k, v in next, data do for k, v in next, data do
if input_cache[k] == nil then local cv = input_cache[k]
if cv == nil then
local node, get_value = create(v) local node, get_value = create(v)
input_nodes[k] = node input_nodes[k] = node
output_cache[k] = transform(get_value, k) output_cache[k] = transform(get_value, k)
elseif input_cache[k] ~= v then input_cache[k] = v
elseif cv ~= v then
set(input_nodes[k], v) set(input_nodes[k], v)
input_cache[k] = v
end end
input_cache[k] = v
end end
local output = {} -- output elements
table.clear(output_array)
for _, v in next, output_cache do for _, v in next, output_cache do
table.insert(output, v) table.insert(output_array, v)
end end
return output return output_array
end end
local function derive() local function derive()
@ -70,50 +72,54 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
return output_get return output_get
end end
-- todo: this should be optimized
local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO } local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () -> K) -> VO): () -> { VO }
local input_cache = {} :: Map<VI, K> local input_cache_up = {} :: Map<VI, K>
local input_cache_buffer_up = {} :: Map<VI, K>
local output_cache = {} :: Map<VI, VO> local output_cache = {} :: Map<VI, VO>
local input_nodes = {} :: Map<VI, Node<K>> local input_nodes = {} :: Map<VI, Node<K>>
local remove_queue = {} :: { VI } local output_array = {} :: { VO }
local function recompute(data: Map<K, VI>) local function recompute(data: Map<K, VI>)
local inverted_data = {} local input_cache, input_cache_buffer =
input_cache_up, input_cache_buffer_up
-- process new or changed values -- process new or changed values
for i, v in next, data do for i, v in next, data do
if input_cache[v] == nil then local cv = input_cache[v]
if cv == nil then
local node, get_value = create(i) local node, get_value = create(i)
input_nodes[v] = node input_nodes[v] = node
input_cache[v] = i
output_cache[v] = transform(v, get_value) output_cache[v] = transform(v, get_value)
elseif input_cache[v] ~= i then elseif cv ~= i then
set(input_nodes[v], i) set(input_nodes[v], i)
end end
inverted_data[v] = i input_cache_buffer[v] = i
end end
-- queue removed values -- remove old values
for v, k in next, input_cache do for v, k in next, input_cache do
if inverted_data[v] == nil then if input_cache_buffer[v] == nil then
table.insert(remove_queue, v) output_cache[v] = nil
input_nodes[v] = nil
end end
end end
-- remove queued values -- update buffer cache
for _, k in next, remove_queue do table.clear(input_cache)
input_cache[k] = nil input_cache_up, input_cache_buffer_up = input_cache_buffer, input_cache
output_cache[k] = nil
input_nodes[k] = nil
end
local output = {} -- output elements
table.clear(output_array)
for _, v in next, output_cache do for _, v in next, output_cache do
table.insert(output, v) table.insert(output_array, v)
end end
return output return output_array
end end
local function derive() local function derive()

View file

@ -176,9 +176,9 @@ local function update_springs(dt: number)
set(output, value) set(output, value)
if new_alpha > 0.9 then -- if new_alpha > 0.9 then
table.insert(remove_queue, data) -- table.insert(remove_queue, data)
end -- end
end end
for _, data in next, remove_queue do for _, data in next, remove_queue do

View file

@ -6,7 +6,7 @@ local BENCH, START = require("test/testkit").benchmark()
local vide = require "src/init" local vide = require "src/init"
local N = 1e6 local N = 2^18 -- 262144
BENCH("Create state", function() BENCH("Create state", function()
local cache = table.create(N) local cache = table.create(N)
@ -17,7 +17,7 @@ BENCH("Create state", function()
end end
end) end)
BENCH("Get state value", function() BENCH("Get value", function()
local state = vide.source(1) local state = vide.source(1)
for i = 1, START(N) do for i = 1, START(N) do
@ -25,7 +25,7 @@ BENCH("Get state value", function()
end end
end) end)
BENCH("Set state value", function() BENCH("Set value", function()
local state = vide.source(1) local state = vide.source(1)
for i = 1, START(N) do for i = 1, START(N) do
@ -45,20 +45,22 @@ BENCH("Derive 1 state", function()
end end
end) end)
BENCH("Derive 2 states", function() BENCH("Derive 4 states", function()
local cache = table.create(N) local cache = table.create(N)
local state = vide.source(1) local state = vide.source(1)
local state2 = vide.source(2) local state2 = vide.source(2)
local state3 = vide.source(3)
local state4 = vide.source(4)
local derive = vide.derive local derive = vide.derive
for i = 1, START(N) do for i = 1, START(N) do
cache[i] = derive(function() cache[i] = derive(function()
return state() + state2() return state() + state2() + state3() + state4()
end) end)
end end
end) end)
BENCH("Set state value derived", function() BENCH("Set derived value", function()
local state = vide.source(1) local state = vide.source(1)
local _derived = vide.derive(state) local _derived = vide.derive(state)
@ -67,7 +69,16 @@ BENCH("Set state value derived", function()
end end
end) end)
BENCH("Apply 4 properties", function() BENCH("Apply 0 properties", function()
local apply = require "src/apply"
local instance = vide.create("Frame") {}
for i = 1, START(N) do
apply(instance, {})
end
end)
BENCH("Apply 8 properties", function()
local apply = require "src/apply" local apply = require "src/apply"
local instance = vide.create("Frame") {} local instance = vide.create("Frame") {}
@ -76,7 +87,11 @@ BENCH("Apply 4 properties", function()
Name = i, Name = i,
Name2 = i, Name2 = i,
Name3 = i, Name3 = i,
Name4 = i Name4 = i,
Name5 = i,
Name6 = i,
Name7 = i,
Name8 = i,
}) })
end end
end) end)
@ -93,4 +108,99 @@ BENCH("Bind state", function()
end end
end) end)
BENCH("Update binding", function()
local apply = require "src/apply"
local instance = vide.create("Frame") {}
local state = vide.source(1)
apply(instance, {
Name = state
})
for i = 1, START(N) do
state(i)
end
end)
BENCH("indexes() no change", function()
local data = {}
for i = 1, N do
data[i] = i
end
local state = vide.source(data)
local _list = vide.values(state, function(v, i)
return {}
end)
START(N)
state(data)
end)
BENCH("indexes() all change", function()
local data = {}
for i = 1, N do
data[i] = i
end
local state = vide.source(data)
local _list = vide.values(state, function(v, i)
return {}
end)
for i, v in data do
data[i] = v + 1
end
START(N)
state(data)
end)
BENCH("values() no change", function()
local data = {}
for i = 1, N do
data[i] = {}
end
local state = vide.source(data)
local _list = vide.values(state, function(v, i)
return {}
end)
START(N)
state(data)
end)
BENCH("values() all change", function()
local data = {}
for i = 1, N do
data[i] = {}
end
local state = vide.source(data)
local _list = vide.values(state, function(v, i)
return {}
end)
for i = 1, N do
local r = math.random(1, #data)
data[i], data[r] = data[r], data[i]
end
START(N)
state(data)
end)
return nil return nil

View file

@ -1054,15 +1054,33 @@ TEST("values()", function()
local input = source { 1, 2, 3 } local input = source { 1, 2, 3 }
local output = values(input, function(v, i) local output = values(input, function(v, i)
return v return { v = v, i = i }
end) end)
input { 1, 2 } input { 1, 2 }
local t = output() local t = output()
CHECK(t[1] == 1) CHECK(t[1].v == 1)
CHECK(t[2] == 2) CHECK(t[2].v == 2)
CHECK(t[3] == nil)
end
do CASE "Removal reflected 2"
local input = source { 1 }
local output = values(input, function(v, i)
return { v = v, i = i }
end)
input { 2, 1 }
input { 1 }
local t = output()
CHECK(t[1].v == 1)
CHECK(t[1].i() == 1)
CHECK(t[2] == nil)
CHECK(t[3] == nil) CHECK(t[3] == nil)
end end
end) end)