03/06/14, 11:02 PM | #1 | |
Join Date: Mar 2014
Posts: 16
|
Window Chaining
Looking through various add-ons, I've noticed that everyone seems to use a generic method chaining function.
I'd like to analyze this function and see if I understand what is happening. Any input/feedback that you guys can provide is much appreciated. The code I'm referring to is part of the "Crystal Fragments Passive" add-on by Lodur. Thanks to him! Here is the code Code:
function CFP.BallAndChain( object ) local T = {} setmetatable( T , { __index = function( self , func ) if func == "__BALL" then return object end return function( self , ... ) assert( object[func] , func .. " missing in object" ) object[func]( object , ... ) return self end end }) return T end Code:
CFP.TLW = CFP.BallAndChain( WINDOW_MANAGER:CreateTopLevelWindow("CFP_BuffDisplay") ) :SetHidden(true) :SetDimensions(w,h) .__BALL 1. We create a new TopLevelWindow (TLW for short) by calling WINDOW_MANAGER:CreateTopLevelWindow("CFP_BuffDisplay"). We pass this Window object to the method chaining function CFP.BallAndChain. 2. The BallAndChain function returns an empty table T...but defines this table's __index metamethod. 3. We attempt to call the SetHidden method on the returned empty table T. Written out explicitly, this would look like T.SetHidden(T, true). 4. Since SetHidden isn't defined in T...we look in the defined metatable for T and look for the __index metamethod. 5. The __index metamethod returns a function. The function returned will first check to see that the original attempted method (in this case SetHidden) is defined in the upvalue variable "object" which is the original TLW Window object. Assuming SetHidden is defined for this window object, SetHidden is executed on the object. Written out explicitly, this would look something like: Window.SetHidden(Window, true). Lastly, the "self" object is returned which is the TLW window object. NOTE: SetHidden doesn't even need to be defined for the Window object...so long as the Window object's __index metamethod eventually leads to the definition of SetHidden. 6. The same process as step 5 is executed for function SetDimensions with parameters w and h. 7. Simplifying, the entire call sort of looks like this: CFP.TLW = CFP.(hidden TLW object with set width and height).__BALL Since __BALL is not defined for this object, we again look at the defined __index metamethod. When the attempted function is __BALL, the Window object is finally returned to CFP.TLW. The final result here is we have created a TopLevelWindow object which has width = w, height = h, and is hidden from the user. Conclusion: It seems like the reason we need this function is because the original creation of the Window via WINDOW_MANAGER:CreateTopLevelWindow("CFP_BuffDisplay") would NOT work as part of a method chain. For example, if we tried to call WINDOW_MANAGER :CreateTopLevelWindow("CFP_BuffDisplay") :SetHidden(true) :SetDimensions(w,h) this would result in an error because WINDOW_MANAGER would be the object passed to each method in the chain, not the actual TLW object created. This is my analysis of what I think is happening. I look forward to you guys tearing this apart and telling me all the places where I am wrong . Thanks again and I look forward to learning more about this. Last edited by inDef : 03/06/14 at 11:32 PM. |
|
03/07/14, 01:43 AM | #2 |
|
I grabbed it from others... I just renamed mine a little. I don't know where it comes from originally.
Last edited by Lodur : 03/07/14 at 01:56 AM. |
03/07/14, 01:47 AM | #3 | |
|
T is just an empty table with a meta table set and the (object == the_control) is only captured in the closure of the anonymous function for the __index function of the metatable. Last edited by Lodur : 03/07/14 at 02:14 AM. Reason: Wrong the first time |
|
03/07/14, 01:50 AM | #4 | |
|
|
|
03/07/14, 05:19 AM | #5 | |
|
Let me clarify :)
I'm the author of the original CHAIN function, harkening back to my WoW "Spoo" addon - let me clarify it for you
It IS a pretty tricky thing, I give you that. But, InDef made a pretty good job at analyzing it! In layman's terms, all the CHAIN does is make a "wrapper" for the table ("object") it receives. The wrapper is a table T (with a metatable on it), whose sole function is to intercept direct calls to the object's functions and force each function call to return the wrapper again, instead of the usual return values, so that more and more calls can be made into it. Ultimately, the __BALL (or simply __END as it was originally) is intercepted to return the object itself, as otherwise we'd be left with the wrapper.
Glad to see my little tangle of code get such attention, have fun with it Oh, one thing that might be interesting - I've had it asked "why doesn't the __index function (self,func) just call the relevant object's function and return the object as results, but instead a whole new function is made and returned? isn't it wasteful?" - well, it IS wasteful. But an __index call doesn't get any parameters that the "methods" need to be called with. Outside code wants to get a function, with Something.Method, not function call results, so it needs to be given a function to call, even if "fake". Consider this: Code:
object:method("param") Code:
m = objec*****thod m (object,"param") ... so if "object" has no "method", just a metatable with __index in it, then __index(object,"method") is called, and the result is returned into m. At this point it needs to be a function, that can get called with (object,"param"). I wonder if I made it clearer or more tangled... :> Last edited by SinusPi : 03/07/14 at 06:59 AM. |
|
03/07/14, 10:12 AM | #6 | ||
Join Date: Mar 2014
Posts: 16
|
|
||
03/07/14, 10:19 AM | #7 | ||
Join Date: Mar 2014
Posts: 16
|
I'm still curious though, it sounds like then that "self" throughout the whole process is the original returned table T. So each step along the chain we're defining new "functions" that will essentially execute the methods (like SetHidden) and then return "self" which if I'm understanding correctly is T. If we are constantly returning functions, at what point do they get executed? For example..in the "Programming in Lua" manual in the "Closures" section, we see the following example: Code:
function newCounter () local i = 0 return function () -- anonymous function i = i + 1 return i end end c1 = newCounter() print(c1()) --> 1 print(c1()) --> 2 So going back to the CHAIN function, when exactly do all of the returned functions in the chain get executed? |
||
03/07/14, 11:58 AM | #8 | |
|
Code:
CFP.TLW = CFP.BallAndChain( WINDOW_MANAGER:CreateTopLevelWindow("CFP_BuffDisplay") ) -- Return T -- Returns T, where T has the __index function on it's meta table built :SetHidden(true) -- step 1) lookup SetHidden via __index: -- return function( self , ... ) -- tmp = T.__index(T, SetHidden) -- 1st anon function runs and returns the 2nd anon function -- Step 2) invoke function call: -- return self -- :tmp(true) -- calls 2nd anon function which calls SetHidden on object and returns self (i.e. T) :SetDimensions(w,h) -- step 1) lookup SetDimensions via __index: -- return function( self , ... ) -- tmp = T.__index(T, SetDimensions) -- 1st anon function runs and returns the 2nd anon function -- Step 2) invoke function call: -- return self -- :tmp(w,h) -- calls 2nd anon function which calls SetDimensions on object and returns self (i.e. T) .__BALL -- step 1) lookup __BALL via __index: -- if func == "__BALL" then return object end -- 1st anon function string matches __BALL names and then returns object. -- There is not invoke function call step as there are no parens after __BALL. Last edited by Lodur : 03/07/14 at 12:02 PM. |
|
03/07/14, 01:45 PM | #9 | ||
Join Date: Mar 2014
Posts: 16
|
The key step I was missing is that "return function (self, ...)" is returning that actual function to some temp variable in memory. So once that is returned we're still left with a function call to tmp(). So if my understanding is right, for the SetHidden call we'd end up with something that looked like: T.tmp(T, true). Since the function "tmp" IS defined for T (since it is explicitly defined right there in the call), tmp is executed...which calls Object.SetHidden(Object, true). It then returns "self" which T so the chain can continue. "Object" is the original window Object passed to the BallAndChain function Does this sound right? If so, I think we've got the analysis of this tricky block of code down!! |
||
03/07/14, 04:35 PM | #10 |
|
Actually, there's a technicality:
Code:
-- tmp = T.__index(T, SetHidden) -- 1st anon function runs and returns the 2nd anon function Code:
-- :tmp(true) -- calls 2nd anon function which calls SetHidden on object and returns self (i.e. T) |
03/07/14, 11:58 PM | #11 | |
|
|
|
08/05/14, 05:13 PM | #12 |
|
Came upon this topic while searching for something completely different, but got interested enough to read through it I'm digging it up because I thought there might be a nicer implementation using a single common metatable. So here's my foot on the platform:
Lua Code:
|
ESOUI » Developer Discussions » General Authoring Discussion » Window Chaining |
«
Previous Thread
|
Next Thread
»
|
Thread Tools | |
Display Modes | |
|
|