Messenger / Observer

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Post Reply
User avatar
rmcode
Party member
Posts: 454
Joined: Tue Jul 15, 2014 12:04 pm
Location: Germany
Contact:

Messenger / Observer

Post 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?
User avatar
ivan
Party member
Posts: 1918
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Messenger / Observer

Post 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)
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Messenger / Observer

Post 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).
TomatoSoup
Prole
Posts: 11
Joined: Sat Jul 19, 2014 2:57 am

Re: Messenger / Observer

Post 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...
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Messenger / Observer

Post 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.
TomatoSoup
Prole
Posts: 11
Joined: Sat Jul 19, 2014 2:57 am

Re: Messenger / Observer

Post 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.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Messenger / Observer

Post 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.
TomatoSoup
Prole
Posts: 11
Joined: Sat Jul 19, 2014 2:57 am

Re: Messenger / Observer

Post 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.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Messenger / Observer

Post 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).
Post Reply

Who is online

Users browsing this forum: Bing [Bot], Google [Bot] and 0 guests