Comments (26)
This should be enabled by default, with maybe an opt-out. It's so trivially easy to footgun with it disabled, as shown in the example. Fusion's simplicity is what makes it powerful. Having to think ages about specific internal workings to find a subtle state update bug does not line up with that simplicity.
from fusion.
I enjoy this because it removes the need for deep equality checking since all the values are just boiled into small Value
objects which can do their own inexpensive referential check only when needed, and it also let's you connect to the change of a single value without an extra computed. Not sure if this would ever be the end all solution for most people, but I definitely run into situations where this is incredibly useful (theme/settings/data tables).
from fusion.
How about something like this for dictionaries, not sure about arrays, but for dictionaries this could work. Not sure if this is actually a viable solution, but I thought I'd bring it up. This function basically turns a table into a table of values recursively, and includes a set function which can be used to update part of the table.
function TableValue(initialTable)
local tableValue = {}
for k, v in pairs(initialTable) do
if (typeof(v) == "table") then
-- Need to implement a check to make sure it's not an array
tableValue[k] = TableValue(v)
else
tableValue[k] = Value(v)
end
end
tableValue.set = function(partial, force)
for key, value in pairs(partial) do
local holder = tableValue[key]
if holder == nil then
continue
end
if (xtypeof(holder) == 'State') then
holder:set(value, force)
else
holder.set(value, force)
end
end
end
return tableValue
end
local myTable = TableValue({ a = 5, b = { c = 5 } })
print("initial a:", myTable.a:get())
Observer(myTable.a):onChange(function()
print("a changed to:", myTable.a:get())
end)
-- This will never update a, so the observer will not fire
myTable.set({ b = { c = 10 })
-- This will update a, but will never update b, or b.c
myTable.set({ a = 0.5 })
from fusion.
I wouldn't create a PR just yet only because this issue is marked as status: needs design
. PR's are generally meant for approved features (a mistake I made twice so just helpin out :D)
from fusion.
As long as it is not the default behavior- I do not want my code to pay a performance penalty for this.
Perhaps the State()
constructor could take in multiple arguments, the second being a dictionary of settings? (Dict so we can potentially add more settings in the future without being a breaking change)
local foo = State(stateArray, {
DeepEquality = true,
})
from fusion.
As long as it is not the default behavior- I do not want my code to pay a performance penalty for this.
Perhaps the
State()
constructor could take in multiple arguments, the second being a dictionary of settings? (Dict so we can potentially add more settings in the future without being a breaking change)local foo = State(stateArray, { DeepEquality = true, })
Perhaps - though I think we should benchmark what performance impact this would have before trying to work around the performance impact :p
Keep in mind this wouldn't affect everything - it'd only affect tables which are not referentially equal.
from fusion.
Thought I'd drop in to give a quick update on this.
It turns out there's a bit of ambiguity over what counts as 'deeply equal' - I was discussing this with one of my friends a while back, and it's actually a more complex and nuanced problem than it appears on the surface, specifically if you want to allow for cycles and table indices and other interesting things.
This 'pureEquals' function should in theory do what we want (thanks to AxisAngles for the help):
local function pair(pairings, a, b)
pairings[a] = b
pairings[b] = a
end
local function unpair(pairings, a, b)
pairings[a] = nil
pairings[b] = nil
end
local function pairable(pairings, a, b)
return pairings[a] == nil or pairings[b] == nil or pairings[a] == b
end
local function next2(a, b, i, j)
local nexti = next(a, i)
local nextj = next(b, j)
if i == nil and j == nil then
if nexti == nil or nextj == nil then
return nil, nil
else
return nexti, nextj
end
elseif nextj ~= nil then
return i, nextj
elseif nexti ~= nil then -- loop back to the beginning
return nexti, next(b)
else
return nil, nil
end
end
local function iCopy(a)
local A = {}
for i, v in next, a do
A[i] = true
end
return A
end
local pureEquals
local pureEqualsTable
-- we can improve the efficiency substantially
function pureEqualsTable(pairings, unpairedA, unpairedB, a, b, i, j, func, ...)
local nexti, nextj = next2(a, b, i, j)
if nexti == nil or nextj == nil then
if next(unpairedA) or next(unpairedB) then
return false
end
-- passed the pairity check, now resume previous routine
if not func then
return true
end
return func(pairings, ...)
end
if unpairedA[nexti] and unpairedB[nextj] then
--assume pairing
unpairedA[nexti] = nil
unpairedB[nextj] = nil
local success = pureEquals(pairings,
nexti, nextj,
pureEquals, a[nexti], b[nextj],
pureEqualsTable, unpairedA, unpairedB, a, b, nexti, nextj, -- should skip to the following i
func, ...)
--unpair cause we're done testing
unpairedA[nexti] = true
unpairedB[nextj] = true
if success then
return true
end
end
--these were not pairable, so now we're going to continue on to the next potential i j pair
return pureEqualsTable(pairings, unpairedA, unpairedB, a, b, nexti, nextj, func, ...)
end
function pureEquals(pairings, a, b, func, ...)
-- if a and b are already paired, then yah, they're paired
if pairings[a] == b then
if not func then
return true
end
return func(pairings, ...) -- resume
elseif pairings[a] ~= nil or pairings[b] ~= nil then
-- if a or b is already paired, then definite failure
return false
end
local typeA = type(a)
local typeB = type(b)
if typeA ~= "table" or typeB ~= "table" then
if a ~= b then
return false
end
if not func then -- definite success
return true
end
return func(pairings, ...) -- resume
end
-- at this point a and b are tables, and not paired to each other
--presume pairity
pair(pairings, a, b)
-- now try to match each element in the table to each other element
local success = pureEqualsTable(pairings,
iCopy(a), iCopy(b), a, b, nil, nil,
func, ...)
-- undo everything
unpair(pairings, a, b)
return success
end
return function(a, b)
return pureEquals({}, a, b)
end
...but, as I'm sure you've noticed, that is a lot of engineering. I did write a substantially shorter function which solves a subset of the problem reasonably well, but which isn't a 'true' deep equals:
local function deepEquals_impl(a, b, seen)
if a == b then
return true
elseif type(a) ~= "table" or type(b) ~= "table" then
return false
end
-- we know `a` and `b` are tables which are not referentially equal
-- time to do a deep check
if seen[a] == nil then
seen[a] = {}
end
if seen[b] == nil then
seen[b] = {}
end
-- if we've seen `a` and `b` before, don't descend into them, because it's a
-- cycle
if seen[a][b] then
return true
end
seen[a][b] = true
seen[b][a] = true
for key, valueA in pairs(a) do
local valueB = b[key]
if not deepEquals_impl(valueA, valueB, seen) then
return false
end
end
for key, valueB in pairs(b) do
local valueA = a[key]
if not deepEquals_impl(valueA, valueB, seen) then
return false
end
end
return true
end
local function deepEquals(a, b)
return deepEquals_impl(a, b, {})
end
return deepEquals
I'm not sure whether to go with this or not - I'm currently still playing around with various ideas. I might end up taking table state in Fusion in a different direction entirely.
tl;dr tables are hard!
from fusion.
I've thought of a different solution. Most of the footguns are from mutating tables. Should Fusion go the Rodux way of making data immutable? This is easy now with table.freeze
.
from fusion.
That's also what I'm considering right now - we would be breaking the mental model of state objects as variables a bit, but perhaps that's a net positive.
from fusion.
Hmm, what if tables as State received a separate class, which has dedicated methods for handling the mutability and recalculations
from fusion.
That's certainly a possibility, but it's worth noting there that your solution doesn't work if the table contains a set
key.
It would be better to externalise the behaviour of setting these table values:
local function makeValues(x)
if typeof(x) ~= "table" then
return Value(x)
else
local tbl = {}
for key, value in pairs(x) do
tbl[key] = makeValues(value)
end
return tbl
end
end
local function syncValues(destination, source)
if xtypeof(destination) == "State" and destination.kind == "Value" then
destination:set(source)
elseif typeof(destination) == "table" then
for key, sub in pairs(destination) do
syncValues(sub, source[key])
end
else
error("Expected a Value object or structure of Value objects")
end
end
local structure = makeValues({
foo = 2,
bar = {
baz = 5,
frob = 7
}
})
print(structure.foo:get()) --> 2
syncValues(structure, {
foo = 6,
bar = {
baz = 2,
frob = 1
}
})
print(structure.foo:get()) --> 6
At that point, this looks like a procedural implementation of state stores - which is certainly another avenue we could pursue. I think this is a very interesting angle to approach from, decomposing values storing tables into tables storing values.
from fusion.
For sure - I think this is a much preferable solution where its applicable.
from fusion.
So just ran into this issue. There might be a simple "maybe hacky" way to solve this.
using HttpService do a JSONEncode of the 2 values in the isSimilar function. Maybe add a few quick checks before the encoding.
Note: I am not sure of the cost of this.
local function isSimilar(a: any, b: any): boolean
if typeof(a) == "table" then
if a == nil and b == nil then
return true
elseif typeof(b) ~= "table" or (a == nil and b ~= nil) or (a ~= nil and b == nil) or #a ~= #b then
return false
end
return HttpService:JSONEncode(a) == HttpService:JSONEncode(b)
else
return a == b
end
end
from fusion.
Doesn't seem like a bad solution, we could do a benchmark with JSONEncode
from fusion.
Also, this would not work 100% for a mixed table but from reading the API doc Roblox while allows it does not like mixed tables.
from fusion.
This solution will not work. You can't JSONEncode Roblox datatypes, meaning that using this would make Fusion unable to have states hold tables with Vector3, Color3, Instances, etc
from fusion.
Might not be perfect but currently if you have a table in a state it always updates the state even if you pass it the same value.
from fusion.
This solution will not work. You can't JSONEncode Roblox datatypes, meaning that using this would make Fusion unable to have states hold tables with Vector3, Color3, Instances, etc
It would be nice if they documented this in there API docs under JSONEncode
and JSONDecode
from fusion.
Oh yeah forgot about that edge case, bit annoying unfortunately.
from fusion.
ok update to check if RB datatypes are in the encode and fallback to og table handling.
local function isSimilar(a: any, b: any): boolean
if typeof(a) == "table" then
if a == nil and b == nil then
return true
elseif typeof(b) ~= "table" or (a == nil and b ~= nil) or (a ~= nil and b == nil) or #a ~= #b then
return false
end
local jsonA, jsonB = HttpService:JSONEncode(a), HttpService:JSONEncode(b)
if jsonA == jsonB and (jsonA:match(":null") or jsonB:match(":null")) then
-- fallback to og return
return false
end
return jsonA == jsonB
else
return a == b
end
end
from fusion.
Non hacky way of handling.
local function _subset(a: table, b: table): boolean
if a == nil and b == nil then
return true
elseif (a == nil and b ~= nil) or (a ~= nil and b == nil) or #a ~= #b then
return false
end
for key, value in a do
if typeof(value) == "table" then
if not isSimilar(b[key], value) then
return false
end
else
if b[key] ~= value then
return false
end
end
end
return true
end
local function isSimilar(a: any, b: any): boolean
if typeof(a) == "table" then
if typeof(b) ~= "table" then
return false
end
return _subset(a, b) and _subset(b, a)
else
return a == b
end
end
from fusion.
This is version would check State Objects in the tables or if for some reason the state object value was a state object.
local class = {}
class.__index = class
function class.tblIsSimilar(a: table, b: table): boolean
if #a ~= #b then
return false
end
for key, value in a do
if not class.isSimilar(b[key], value) then
return false
end
end
return true
end
function class.isSimilar(a: any, b: any): boolean
if a == nil and b == nil then
return true
elseif typeof(a) ~= typeof(b) then
return false
elseif a.type == "State" and b.type == "State" then
return class.isSimilar(a:get(), b:get())
elseif typeof(a) == "table" and typeof(b) == "table" then
return class.tblIsSimilar(a, b) and class.tblIsSimilar(b, a)
else
return a == b
end
end
return class.isSimilar
from fusion.
if you are good with this last version I can do a pull request.
from fusion.
I wouldn't create a PR just yet only because this issue is marked as
status: needs design
. PR's are generally meant for approved features (a mistake I made twice so just helpin out :D)
no worries. Even if what I wrote just helps some with the design choices I am glad to help.
from fusion.
The correct way to solve this problem should be to let the user pass its own equality function (on both Value
s and Computed
s). A framework should not be the one deciding how to perform equality checks. I have a draft of this already.
from fusion.
From my. conversations with others, there seems to be little appetite for this. Closing in favour of #291.
from fusion.
Related Issues (20)
- Typechecking capabilities are dramatically reduced in v0.3 HOT 1
- Should Value:set with force also trigger Computed onChange? HOT 1
- ForValues is not passing 'use' - only value is being passed on the first argument HOT 1
- Fusion for general Lua(u) environments HOT 4
- Support for Roact-like/Class-based components HOT 4
- Allow the Value type to specify what it can be set to HOT 1
- OnChange usage example is the same as the example for OnEvent
- Generalised value destruction HOT 16
- New state objects downstream from in-progress updates see inconsistent dependencies HOT 16
- NaN values break springs HOT 1
- Error boundaries HOT 1
- Look into fuzzing as part of our testing suite
- Add explanations and breakdowns of example place files
- Spring/Tween flickering with hydrated reference HOT 2
- Seems that Fusion 0.3 rbxm file is marked as 0.2 HOT 2
- Add unit tests for NaN self-similarity
- Computed does not capture dependency when Value created inside the callback HOT 2
- Fusion.Tween does not catch state update in certain cases
- Frozen tables should use reference equality
- Fusion.Children should parent things in batching order HOT 7
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from fusion.