Thread Tools Display Modes
03/06/14, 11:02 PM   #1
inDef
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
And an example call:

Code:
  CFP.TLW = CFP.BallAndChain( 
      WINDOW_MANAGER:CreateTopLevelWindow("CFP_BuffDisplay") )
    :SetHidden(true)
    :SetDimensions(w,h)
  .__BALL
So here is what I THINK is happening from call to completion:

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.
  Reply With Quote
03/07/14, 01:43 AM   #2
Lodur
AddOn Author - Click to view addons
Join Date: Feb 2014
Posts: 108
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.
  Reply With Quote
03/07/14, 01:47 AM   #3
Lodur
AddOn Author - Click to view addons
Join Date: Feb 2014
Posts: 108
Originally Posted by inDef View Post

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.
Right. T is returned, so the chain may continue. self == T. object is only returned at the end.

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
  Reply With Quote
03/07/14, 01:50 AM   #4
Lodur
AddOn Author - Click to view addons
Join Date: Feb 2014
Posts: 108
Originally Posted by inDef View Post
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.
Right in that if every method on the control did return self then chaining would not be needed.
  Reply With Quote
03/07/14, 05:19 AM   #5
SinusPi
AddOn Author - Click to view addons
Join Date: Feb 2014
Posts: 18
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.

Originally Posted by Lodur
Right in that if every method on the control did return self then chaining would not be needed.
Rather - it'd be chainable by default (like jQuery is). it's extra chain-wrapping that would not be needed.


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")
is the same as
Code:
 m = objec*****thod
 m (object,"param")
So, it first accesses "method" in "object"...
... 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.
  Reply With Quote
03/07/14, 10:12 AM   #6
inDef
Join Date: Mar 2014
Posts: 16
Originally Posted by Lodur View Post
Right. T is returned, so the chain may continue. self == T. object is only returned at the end.

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.
So if the T object is all that is getting returned in each part of the chain, when do the new functions from the __index metamethod get returned?
  Reply With Quote
03/07/14, 10:19 AM   #7
inDef
Join Date: Mar 2014
Posts: 16
Originally Posted by SinusPi View Post
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.


Rather - it'd be chainable by default (like jQuery is). it's extra chain-wrapping that would not be needed.
Ok I think I understand. The chain "wrapper" forces any method called as part of the chain to return "self" as the object. This allows us to call many methods on a particular object, without having to modify those functions to return "self" so the chain can continue.

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
From this example we can see that when c1 is created, the returned function is not actually executed. The function isn't executed for the first time until c1() is evaluated as the parameter to the print() statement.

So going back to the CHAIN function, when exactly do all of the returned functions in the chain get executed?
  Reply With Quote
03/07/14, 11:58 AM   #8
Lodur
AddOn Author - Click to view addons
Join Date: Feb 2014
Posts: 108
Originally Posted by inDef View Post

So going back to the CHAIN function, when exactly do all of the returned functions in the chain get executed?
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.
That is my understanding, at least...

Last edited by Lodur : 03/07/14 at 12:02 PM.
  Reply With Quote
03/07/14, 01:45 PM   #9
inDef
Join Date: Mar 2014
Posts: 16
Originally Posted by Lodur View Post
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.
That is my understanding, at least...
This explained what was going on perfectly.

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!!
  Reply With Quote
03/07/14, 04:35 PM   #10
SinusPi
AddOn Author - Click to view addons
Join Date: Feb 2014
Posts: 18
Actually, there's a technicality:

Code:
 -- tmp = T.__index(T, SetHidden) -- 1st anon function runs and returns the 2nd anon function
This here isn't really an anon function - it's a "normal" function in the metatable. You can get it with getmetatable(CHAIN(boo)).__index (if you'd ever need it, of course).
Code:
-- :tmp(true)  -- calls 2nd anon function which calls SetHidden on object and returns self (i.e. T)
Now THIS is a proper anon function. And wasteful, too - each time you call something in the chain, a new function gets made to handle that specific call. But it's quick and easy to use, so why worry about being optimal...
  Reply With Quote
03/07/14, 11:58 PM   #11
Lodur
AddOn Author - Click to view addons
Join Date: Feb 2014
Posts: 108
Originally Posted by SinusPi View Post
Actually, there's a technicality:

Code:
 -- tmp = T.__index(T, SetHidden) -- 1st anon function runs and returns the 2nd anon function
This here isn't really an anon function - it's a "normal" function in the metatable. You can get it with getmetatable(CHAIN(boo)).__index (if you'd ever need it, of course).
Ah yes. I agree. Made me search for the exact definition too.
  Reply With Quote
08/05/14, 05:13 PM   #12
merlight
AddOn Author - Click to view addons
Join Date: Jul 2014
Posts: 671
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:
  1. local ChainMT =
  2. {
  3.     __index = function(self, key)
  4.         local object = assert(rawget(self, "__BALL"))
  5.         local method = assert(object[key], key .. " missing in object")
  6.         return function(self, ...)
  7.             method(object, ...)
  8.             return self
  9.         end
  10.     end
  11. }
  12.  
  13. function BallAndChain(object)
  14.     local T = { __BALL = object }
  15.     return setmetatable(T, ChainMT)
  16. end
  Reply With Quote

ESOUI » Developer Discussions » General Authoring Discussion » Window Chaining


Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off