Page 1 of 1
Messenger / Observer
Posted: Sat Mar 05, 2016 1:41 pm
by rmcode
I've been using this class to distribute messages between classes and reduce some of the coupling:
Code: Select all
local Messenger = {};
local subscriptions = {};
function Messenger.publish( message, ... )
for _, subscription in ipairs( subscriptions ) do
if subscription.message == message then
subscription.callback( ... );
end
end
end
function Messenger.observe( message, callback )
subscriptions[#subscriptions + 1] = { message = message, callback = callback };
end
return Messenger;
It works fine for what it originally was being used for, but now I need to extend it. My problem is that when an object with an open subscription is "removed" the subscription still is open.
Subscriptions are added via:
Code: Select all
Messenger.observe( EVENT.GRAPH_UPDATE_CENTER, function( ... )
updateCenter( ... );
end)
Any idea how to extend my class accordingly? Or does anybody have a recommendation for a lib?
Re: Messenger / Observer
Posted: Sat Mar 05, 2016 2:56 pm
by ivan
I posted about a
similar technique in another topic.
Looks like the "Messenger" functionality is not that useful, and it doesn't store its data in a very good way.
One thing you could try is to index the subscriptions by message type, and the subscribers by a 'receiver' reference.
Code: Select all
function Messenger.setSubscription( message, receiver, callback )
local subs = subscriptions[message] or {} -- new message type?
subscriptions[message] = subs -- make sure we index it
subs[receiver] = callback -- store the callback
end
This way you also have a reference to the receiver object when the message is broadcast:
Code: Select all
function Messenger.publish( message, ... )
local subs = subscriptions[message] -- subscribers listening for this message
if subs == nil then return end -- no subs for this message!
for receiver, callback in pairs( subs ) do
callback ( receiver, ... ) -- pass back the receiver reference as the first argument
end
end
Since the 'receiver' reference is passed back you need to use this accordingly:
Code: Select all
function someObject:init()
Messenger.setSubscription('someMessage', self, self.receive)
end
function someObject:receive(...)
...
end
To 'unsubscribe' you just write:
Code: Select all
Messenger.setSubscription( message, receiver, nil)
Re: Messenger / Observer
Posted: Sat Mar 05, 2016 4:56 pm
by airstruck
A simple solution could be returning the subscription object from Messenger.observe, and adding a function like Messenger.remove that takes a subscription object as an argument.
Code: Select all
local Messenger = {};
local subscriptions = {};
function Messenger.publish( message, ... )
for _, subscription in ipairs( subscriptions ) do
if subscription.message == message then
subscription.callback( ... );
end
end
end
function Messenger.observe( message, callback )
local sub = { message = message, callback = callback };
subscriptions[#subscriptions + 1] = sub
return sub
end
function Messenger.remove( subscription )
for k, v in ipairs(subscriptions) do
if v == subscription then
table.remove(subscriptions, k)
return
end
end
end
return Messenger;
When you create an event handler that will need to be removed at some point, store the return value from Messenger.observe so you can pass it to Messenger.remove later. If you want, give subscriptions their own "remove" methods so you don't need a handle to the Messenger to remove them (might save you a "require" somewhere).
Re: Messenger / Observer
Posted: Sat Mar 05, 2016 6:08 pm
by TomatoSoup
Have you considered making subscriptions a weak list and having the creator of a subscription maintain a reference to it? Then, when you destroy the creator, the garbage collector will eventually swing by and remove it.
A consequence would be that you'd have to change ipairs to regular pairs, but besides that...
Re: Messenger / Observer
Posted: Sat Mar 05, 2016 7:13 pm
by airstruck
TomatoSoup wrote:A consequence would be that you'd have to change ipairs to regular pairs, but besides that...
If you stored the length of the array somewhere and skipped nil values when iterating you could use `for i = 1, n`, and you could compact the array every now and then.
The problem with relying on the GC is there's no good way to get the handler to stop handling events exactly when you want it to. You could end up with some weird "ghost behavior," for example where some entities act like other entities that were removed are still there.
Of course you could manually run the GC with collectgarbage, but if you're going to do it manually anyway, you might as well do something more straightforward like subscription:remove().
There's also the possibility that once you expose the subscription, another reference to it accidentally gets created somewhere along the line, leading to the handler hanging around much longer than intended and causing unexpected behavior that could be hard to track down.
Re: Messenger / Observer
Posted: Sat Mar 05, 2016 7:55 pm
by TomatoSoup
The array would remain nicely compact even without doing that. In t = {1,2,nil,4,5,nil}, t[#t+1] would insert to the third slot, and then again would insert into the sixth slot. It'd also guarantee that you never accidentally start putting values into the hashmap part of the table.
Perhaps an alternate option might be for the creator to pass itself to the messenger that it creates. Whenever the messenger wants to fire, it first checks if its creator is nil or not. For what it's worth, I think that the sporadic nature of messages probably means that waiting for the GC to hit it will be fine. But you're right that it doesn't provide any guarantees and that can be an outright deal breaker.
Re: Messenger / Observer
Posted: Sat Mar 05, 2016 8:10 pm
by airstruck
TomatoSoup wrote:The array would remain nicely compact even without doing that. In t = {1,2,nil,4,5,nil}, t[#t+1] would insert to the third slot, and then again would insert into the sixth slot. It'd also guarantee that you never accidentally start putting values into the hashmap part of the table.
That
might happen, but
there is no guarantee.
The length of a table t is defined to be any integer index n such that t[n] is not nil and t[n+1] is nil
In other words, given t = {1,2,nil,4,5,nil}, #t will be either 2 or 5; the actual result is implementation-dependent and is not even necessarily going to be the same each time #t is evaluated over the program's lifetime.
Anyway, I agree that waiting on the GC would be alright in cases where all handlers essentially have no effect and can run without error during the time when they're no longer needed and waiting to be collected. I'm not sure how common that situation is going to be in practice, though.
Re: Messenger / Observer
Posted: Sat Mar 05, 2016 10:49 pm
by TomatoSoup
You're right, it's implementation specific. What I more meant to establish was that you wouldn't overwrite existing things, though. And that, with the Love2d Win64 implementation, it tends to work out. I wrote a script that'd randomly insert and remove and, by my best estimate, it works as I described. But there's no guarantees for the same .love in any other platform. So, yeah. It's safe, but it doesn't guarantee the niceness that I first attributed to it.
Re: Messenger / Observer
Posted: Sat Mar 05, 2016 11:11 pm
by airstruck
Under Lua 5.1.5 I get 2 for #t, and under LuaJIT 2.0.3 I get 5. You could use #t if you didn't care where things went into the array, but you'd still need to condense it. Calling those handlers in an arbitrary order is something you'd have to watch, things could work if one handler runs before another but break if it runs later. You also won't be able to give event handlers the ability to cancel the event in a reliable way if you decide you need that later (some of the same problems as pairs).