Fusion icon indicating copy to clipboard operation
Fusion copied to clipboard

Limiting state writing

Open dphfox opened this issue 3 years ago • 4 comments

Currently, set and get operations are present as methods on a single state object:

local value = Value(5)

value:set(15)
print(value:get()) -- 15

However, this poses a problem: what if we don't want to give full write permissions to someone we're giving our state objects to?

local function makeReadOnly()
    local readOnly = Value(0)
    task.defer(function()
        repeat
            readOnly:set(readOnly:get() + 1)
        until false
    end)
    return readOnly
end

local readOnly = makeReadOnly()

print(readOnly:get()) -- ok
readOnly:set(9001) -- oh no - the contract is not enforced

Users end up having to get inventive with weird solutions like passthrough Computed objects, which aren't really great:

local function makeReadOnly()
    local readOnly = Value(0)
    task.defer(function()
        repeat
            readOnly:set(readOnly:get() + 1)
        until false
    end)
    -- this would seem unnecessary to a casual reader
    return Computed(function()
        return readOnly:get()
    end)
end

local readOnly = makeReadOnly()

print(readOnly:get()) -- ok
readOnly:set(9001) -- errors - that's better :)

It might be worth investigating some way of separating out the reading and writing abilities of a state object. One solution could be adopting React/Solid-like setter/getter splitting (open question: how does this translate to objects with more complex setting behaviour, for example a spring where we might want to read and write aspects like velocity and position?)

local function doLimited(getX)
    -- can't change the value of x here, can only read from it
    print("x is", getX())
end

local getX, setX = Value(10)

doLimited(getX) -- the value of x is 10
setX(25)
doLimited(getX) -- the value of x is 25

Another solution, perhaps more Fusion-like, would be introducing a utility ReadOnly function that creates a limited version of a state object for the user - this would almost certainly be optimised under the hood:

local function doLimited(x)
    -- can't change the value of x here, can only read from it
    print("x is", x:get())
end

local myValue = Value(10)
local limitedValue = ReadOnly(myValue)

doLimited(limitedValue) -- x is 10
myValue:set(25)
doLimited(limitedValue) -- x is 25

dphfox avatar Dec 28 '21 01:12 dphfox

It's worth mentioning that a solution here could possibly relate partially to #35, as changing how we retrieve values could potentially also change how we treat constants versus state.

dphfox avatar Dec 28 '21 01:12 dphfox

Another idea I like, though it doesn't address all the motivating problems, is the ability for value objects to give away write control over their value, turning the object itself read-only:

local foo = Value(5)

local access = foo:giveMeExclusiveWriteControl()

access:set(10)
print(peek(foo)) -- 10
foo:set(2) -- error

dphfox avatar Aug 22 '23 21:08 dphfox

I think it makes more sense to do that as local foo, access = ReadOnly(). One-time operations on a State don't fit well in the reactive graph, and I can't imagine many cases where you wouldn't want to do it as one of the first things when a Value is created anyways.

Dionysusnu avatar Aug 23 '23 00:08 Dionysusnu

I think it makes more sense to do that as local foo, access = ReadOnly(). One-time operations on a State don't fit well in the reactive graph, and I can't imagine many cases where you wouldn't want to do it as one of the first things when a Value is created anyways.

Mostly I'm thinking of use cases where you pass a value into a function call with the expectation that the function call starts maintaining that value for you, in which case it becomes a way of enforcing strongly that two agents don't have write access at the same time. Think e.g. [Ref].

dphfox avatar Aug 23 '23 10:08 dphfox