Is it possible to make a collapsing variables without making individual functions?

前端 未结 2 1826
既然无缘
既然无缘 2021-01-25 05:55

I have a code that starts as a small amount of variables and makes more elements using those initial variables.

function new( x, y, width, height )
    local obj         


        
相关标签:
2条回答
  • 2021-01-25 06:24

    I was bored and saw this question (again) along with a duplicate. I started writing some code for fun, leading to this:

    local function getNeededVars(tab,func)
        local needed,this = {}
        this = setmetatable({},{
            __index = function(s,k)
                -- See if the requested variable exists.
                -- If it doesn't, we obviously complain.
                -- If it does, we log it and return the value.
                local var = tab.vars[k]
                if not var then
                    error("Eh, "..k.." isn't registered (yet?)",5)
                end needed[k] = true return tab.vals[k]
            end;
        }) func(this) return needed
    end
    
    local function updateStuff(self,key,done)
        for k,v in pairs(self.levars) do
            if v.needed and v.needed[key] then
                if not done[v] then done[v] = true
                    self.vals[v.name] = v.func(self)
                    updateStuff(self,v.name,done)
                end
            end
        end
    end
    
    local function createSubTable(self,key,tab)
        return setmetatable({},{
            __newindex = function(s,k,v)
                tab[k] = v updateStuff(self,key,{})
            end; __index = tab;
        })
    end
    
    local dependenceMeta
    dependenceMeta = {
        __index = function(self,k)
            -- Allow methods, because OOP
            local method = dependenceMeta[k]
            if method then return method end
            local variable = self.vars[k]
            if not variable then
                error("Variable "..k.." not found",2)
            end return self.vals[k]
        end;
        __newindex = function(self,k,v)
            local variable = self.vars[k]
            if not variable then
                error("Use :Register() to add stuff",2)
            elseif type(v) == "table" then
                self.vals[k] = createSubTable(self,k,v)
                return updateStuff(self,k,{})
            end self.vals[k] = v updateStuff(self,k,{})
        end
    }
    function dependenceMeta:Register(var,value)
        local varobject = {func=value,name=var}
        self.vars[var] = varobject
        table.insert(self.levars,varobject)
        if type(value) == "function" then
            varobject.needed = getNeededVars(self,value)
            self.vals[var] = value(self)
        elseif type(value) == "table" then
            self.vals[var] = createSubTable(self,var,value)
        elseif value then
            self.vals[var] = value
        end
    end
    function dependenceMeta:RegisterAll(tab)
        for k,v in pairs(tab) do
            self:Register(k,v)
        end
    end
    
    local function DependenceTable()
        return setmetatable({
            levars = {};
            vars = {};
            vals = {};
        },dependenceMeta)
    end
    
    local test = DependenceTable()
    test:Register("border",{
        x=20; y=50;
        height=200;
        width=100;
    })
    test:Register("body",function(self)
        return {x=self.border.x+1,y=self.border.y+1,
            height=self.border.height-2,
            width=self.border.width-2}
    end)
    test:Register("font",function(self)
        local size = (self.body.height+2)-(math.floor((self.body.height+2)/4)+1);
        return { size = size; -- Since we use it in the table constructor...
            height = size-4; --love.graphics.setNewFont( self.font.size ):getHeight();
            -- I don't run this on love, so can't use the above line. Should work though.
        }
    end)
    test:Register("padding",function(self)
        local height = math.floor(self.border.height*(2/29))
        return { height = height; width = height*3 } -- again dependency
    end)
    test:Register("text",{input=""}) -- Need this initially to keep input
    test:Register("text",function(self)
        return { input = self.text.input;
            centerHeight = math.ceil(self.body.y+((self.body.height-self.font.height)/2));
            left = self.body.x+self.padding.width+self.padding.height;
        }
    end)
    test:Register("backspace",{key = false, rate = 3, time = 0, pausetime = 20, pause = true})
    -- Again, didn't use gui.id() on the line below because my lack of LÖVE
    test:Register("config",{active=true,devmode=false,debug=false,id=123,type='textbox'})
    
    print("border.x=20, test.text.left="..test.text.left)
    test.border = {x=30; y=50; height=200; width=100;}
    print("border.x=30, test.text.left="..test.text.left)
    test.border.x = 40
    print("border.x=40, test.text.left="..test.text.left)
    

    It's a lot of code, but I liked writing it. It gives this nice output:

    border.x=20, test.text.left=73
    border.x=30, test.text.left=83
    border.x=40, test.text.left=93
    

    All properties only get recalculated when one of its dependencies is edited. I made it also work with subtables, which was a bit tricky, but at the end actually seems quite easy. You can edit (for example) the body field by setting it to a completely new table or by setting a field in the already existing table, as seen in the last few lines of the code snippet. When you assign it to a new table, it'll set a metatable on it. You can't use pairs (& co) neither, unless you use 5.2 and can use __pairs.

    It might solve your problem. If not, I had fun writing it, so at least it'll always be something positive that I wrote this. (And you have to admit, that's some beautiful code. Well, the way it works, not the actual formatting)

    Note: If you're gonna use it, uncomment the love.graphics and gui.id part, as I don't have LÖVE and I obviously had to test the code.

    Here's a quick "summary" of my thing's API, as it might be confusing in the beginning:

    local hmm = DependenceTable() -- Create a new one
    print(hmm.field) -- Would error, "field" doesn't exist yet
    
    -- Sets the property 'idk' to 123.
    -- Everything except functions and tables are "primitive".
    -- They're like constants, they never change unless you do it.
    hmm:Register("idk",123)
    -- If you want to actually set a regular table/function, you
    -- can register a random value, then do hmm.idk = func/table
    -- (the "constructor registering" only happens during :Register())
    
    -- Sets the field to a constructor, which first gets validated.
    -- During registering, the constructor is already called once.
    -- Afterwards, it'll get called when it has to update.
    -- (Whenever 'idk' changes, since 'field' depends on 'idk' here)
    hmm:Register("field",function(self) return self.idk+1 end)
    -- This errors because 'nonexistant' isn't reigstered yet
    hmm:Register("error",function(self) return self.nonexistant end)
    -- Basicly calls hmm:Register() twice with key/value as parameters
    hmm:RegisterAll{
        lower = function(self) return self.field - 5 end;
        higher = function(self) return self.field + 5 end;
    }
    -- This sets the property 'idk' to 5.
    -- Since 'field' depends on this property, it'll also update.
    -- Since 'lower' and 'higher' depend on 'field', they too.
    -- (It happens in order, so there should be no conflicts)
    hmm.idk = 5
    -- This prints 6 since 'idk' is 5 and 'field' is idk+1
    print(hmm.field)
    

    You could use setfenv (if Lua 5.1) to remove the need of 'self.FIELD'. With some environment magic you can have the constructor for 'field' (as an example) just be function() return idk+1 end.

    0 讨论(0)
  • 2021-01-25 06:26

    You could make use of metatables, more specific, the __newindex field:

    (Well, need to combine it with the __index field, but eh)

    function new(x, y, width, height )
        local object = {
            font = {}, padding = {}, text = {input=''}, -- tables themself are static
            -- also I assume text.input will change and has to stay the way it is
        }
        -- more static data here (yes yes, I know. The code is a bit ugly, but if it works fine...)
        object.config = { active = true, devmode = false, debug = false, id = gui.id(), type = 'textbox' }
        object.backspace = {key = false, rate = 3, time = 0, pausetime = 20, pause = true}
            object.border = { x = x, y = y, width = width, height = height }
        -- stuff that has to be calculated from the above variables goes below
        local border = object.border
        local function calculate()
            --border
            --body
            object.body = { x = border.x+1, y = border.y+1, width = border.width-2, height = border.height-2 }
            --font
            object.font.size = height-(math.floor(height/4)+1)
            object.font.height = love.graphics.setNewFont( object.font.size ):getHeight()
            --padding
            object.padding.height = math.floor(object.border.height*(2/29))
            object.padding.width = object.padding.height*3
            --text
            object.text.centerHeight = math.ceil(object.body.y+((object.body.height-object.font.height)/2))
            object.text.left = object.body.x+object.padding.width+object.padding.height
            --backspacing
            --config
        end
        calculate()
        local proxy = setmetatable({},{
            __index = object; -- proxy.abc returns object.abc (to get width, use proxy.border.width)
            __newindex = function(s,k,v)
                -- fires whenever 'proxy[k] = v' is done
                -- I assume you'll only change x/y/width/height, as other properties are dynamic
                -- Doing 'proxy.x = 123' is the same as 'object.border.x = 123' + recalculating
                object.border[k] = v -- Actually apply the change
                calculate() -- Recalculate the other properties that depends on the above
            end;
        })
        gui.add(object)
        return object.config.id
    end
    

    You can run code like proxy.x = 12 to edit the X property. All values will be recalculated. It's not the best, but your code a tiny bit annoying to improve. (But hey, if it works fine for you, it's good)

    Note: You can only set x, y, width and height. You can get all properties the old way though, e.g. proxy.padding.width (Mind that proxy.x doesn't work. Use proxy.border.x)

    0 讨论(0)
提交回复
热议问题