Reputation: 97601
Lets say I have some "object" that I've defined elsewhere. Maybe it represents a set of items, but is more complex than a simple table. Whatever it may be, it would be logical to iterate over it.
As such, it has a iterator
method defined. So I can write this:
local myObject = AbstractObject:new()
for obj in myObject:iterator() do
obj:foo()
end
What I'm wondering is if there is some metamethod trickery that I can do, which will allow me to write this:
local myObject = AbstractObject:new()
for obj in myObject do
obj:foo()
end
So is there?
Upvotes: 4
Views: 412
Reputation: 10695
One slight change to your example would make the semantics a lot less painful:
local myObject = AbstractObject:new()
for obj in myObject() do
obj:foo()
end
That way, you can use a metatable to define the __call
metamethod to return myObject:interator()
, with code that looks something like this in AbstractObject:new()
:
setmetatable(newobject, {__call = function() return newobject:iterator() end})
Without the iterator construction, you'll be effectively reusing a single iterator for multiple iterations, which means you'll need to keep the iterator state in the object/creation closure, and reset it after it finishes so the next call will restart the iteration again. If you really want to do this, the best solution would really be to write something for the specific iteration implementation, but this would perform the generic iteration:
local iterator
--table.pack is planned for 5.2
local pack = table.pack or function(...)
local t = {...}
t.n = select('#',...)
return t
end
--in 5.1 unpack isn't in table
local unpack = table.unpack or unpack
function metamethods.__call(...)
if not iterator then
iterator = newobject:iterator()
end
local returns = pack(iterator(...))
if returns[1] == nil then
--iteration is finished: next call will restart iteration
iterator = nil
end
return unpack(returns, 1, returns.n)
end
Again: This should really be adjusted to fit your use case.
Upvotes: 3
Reputation: 74770
The object used after in
must be a function, which will be called repeatedly by the generic for
loop.
I'm not sure if you can make a table or user object callable like a function, but even then the problem would be that your object can only have one internal iterator state - i.e. it would not allow multiple iterations over the same object (neither concurrently nor sequentially), unless you are somehow explicitly resetting it.
As answered by Stuart, you could use the __call
metamethod suitably to return the iterator, but then you would have to write
for obj in myObject() do
obj:foo()
end
This is not quite what we want.
Reading a bit more in PiL, I see that there are more components used in the for loop: the invariant loop state, and the current value of the control variable, which are passed to the iterator function in each call. If we don't provide them in the in
expression, they are initialized to nil
.
Thus, my idea would be to use these values to distinguish the individual calls.
If you can create a next(element)
function for your collection which returns for each element the next one, the implementation would be simple:
metatable.__call = function(_state, _last)
if(_last == nil) then
return obj:first()
else
return obj:next(_last)
end
end
But often we would not have something like this, then it gets more complicated.
I thought about using coroutines here, but these still need a factory method (which we want to avoid). It would result in something similar like what Stuart wrote (i.e. saving the iterator state somewhere in the object itself or in some other variable related to the object), and using the parameter and/or the iterators result to decide when to create/clean the iterator object/state.
Nothing won here.
Upvotes: 0