So I can create a Callable with a lambda, which I can then pass to Signal.connect. Is this a bad idea?

The docs show create_tween().tween_callback(func(): my_dictionary.clear()) which is a similar concept though not the same mechanism. With a signal that would be some_signal.connect(func(): my_dictionary.clear()). I worry if this would be a bad practice. I don't know how to or if I can remove a Callable without a name from a Signal, and later on what happens if the Signal emits but my Object which provided the lambda is gone?

Maybe if I bind a Callable to itself and have it disconnect itself? Would that work? Something like

	var button:Button = $"Button"
	var callable := func (p_this:Callable, p_button:Button):
		if p_button.pressed.is_connected(p_this):
			p_button.pressed.disconnect(p_this)
	
	callable.bindv([callable, button])
	button.pressed.connect(callable)

I'm gonna test this but please stop me if this is a terrible idea.

    award what happens if the Signal emits but my Object which provided the lambda is gone

    Then the signal would be ignored, unless it has a listener in another object. It's the same as connecting a normal method as the listener, and then deleting the object that contains the method.

    It's possible to get a reference to a Lambda signal listener, if you really need it.

    wait_timer.timeout.connect(func(): run_state_machine(StateAction.WAIT_COMPLETED))
    print_debug(wait_timer.timeout.get_connections())

    [{ "signal": Timer::[signal]timeout, "callable": <anonymous lambda>(lambda), "flags": 0 }]

      The result of my attempt:

      E 0:00:06:0153   Object::emit_signalp: Error calling from signal 'pressed' to callable: 'GDScript::': Method expected 0 arguments, but called with 0.
        <C++ Source>   core\object\object.cpp:1080 @ Object::emit_signalp()

      "Method expected 0 arguments, but called with 0" 😂 Wonderful. Bad idea to bind things to a lambda?

      Here's how it works:

      • if a ambda does not use anything from the object, it will survive object deletion if a signal is connected to it.
      • if it uses something from the object, it will be gone when object is deleted and signal will automatically be disconnected.

        DaveTheCoder
        xyz

        Ooh slick. So I can connect a lambda and not worry about cleanup then? Assuming that I don't keep trying to connect it repeatedly or otherwise make a horrible mess of things.

        I'm impressed that GDScript takes care of that. C# delegates would just let me shoot myself in the foot.

        • xyz replied to this.

          award Yeah, it works neatly. This is actually a very good use case for lambdas.

          I've realized that the problem with my original code was not doing
          callable = callable.bindv([callable, button])
          If I actually connect the bound copy then it works as intended

          DaveTheCoder

          I think it makes sense. The signal has a reference to the callable. The callable may have references to the object. If the object is deleted, and the callable has references to it, then calling the callable would lead to errors or undefined behavior. If it doesn't have references to the object, then it should still be able to run on its own fine.

          I suspect it's deliberately implemented this way rather than a byproduct of reference-counting.

          DaveTheCoder Are you sure? That sounds backwards.

          Test it out.

          Callable can be a method or a standalone function. If it's associated with an object, then it's considered a method. You can check if there is object association via Callable::get_object(). If a lambda is accessing any properties it will be magically treated as a method. Otherwise as a standalone function.

          So if a connected lambda has an object association (is a method), the signal connection will be broken when the associated object is destroyed. If there is no object association then the labmda will live "forever".

          a year later

          There's a corner case to add to xyz's description:

          If a lambda only accesses a second lambda that in turn accesses properties, you'll get a crash (as of 4.3). The system isn't clever enough to see that the first lambda accesses the object through the second lambda, so it doesn't break the signal connection when the associated object is destroyed.

          Example:

          # Level_1.gd
          func _ready():
              var respawn = func(ship_id):
                  var new_ship: Ship = self.ship_scene.instantiate()
                  new_ship.ship_id = ship_id
                  self.add_child(new_ship)
              Signals.ship_died.connect(func(ship): respawn.call(ship.ship_id))

          In the above code, func(ship): respawn.call(ship.ship_id) doesn't directly refer to self, so when the Level_1 instance is destroyed, the Signals.ship_died connection is not broken. The next time Signals.ship_died is emitted, the lambda attempts to access respawn on the destroyed Level_1 instance. This results in

          Attempt to call function `<anonymous lambda>(lambda) (Callable)' on a null instance.

          The workaround is to make respawn a method, or to put a dummy reference to self in the connected lambda.

          • xyz replied to this.

            jumpingmechanic It's not about "cleverness" of the system.

            The system can only know about the function that is actually connected. If this function is a method associated with an object, then it can be disconnected in the case of that object's destruction. If you connect an object-less lambda (like in your example), then the connection is not associated with any objects. The fact that lambda's code is written in some class, doesn't make it associated with that object. (That's in fact the logical error in your reasoning). That lambda has no concept of self object and the object holds no reference to that lambda. From the object's point of view, the signal connection to that lambda is as if it was connected to some other object. So it's not that object's responsibility to disconnect the signal connected to the lambda.

            Tldr, the "cleverness" you here expected from the system would actually make no sense and is not feasible.

              xyz Thanks for explaining!

              You mentioned earlier: "If a lambda is accessing any properties it will be magically treated as a method". If the lambda being written in class A doesn't get any special relationship with the A instance, what exactly is the magic "upgrade" that happens to the lambda when it refers to self? (I found that func(): print(self) gets auto-disconnected, while func(): print(4) does not.) What if the lambda refers both to the self and a B instance - which one becomes the associated object?

              • xyz replied to this.

                jumpingmechanic The object association is stored with every Callable object. You can get it via Callable::get_object(). If you use the keyword self inside lambda body it will get associated with the object running the script (i.e. the object self refers to in that context). Otherwise it won't get associated with any object and get_object() will just return a reference to the Script object its code is written in.

                So it doesn't matter what object(s) the lambda is referencing. What matters is the usage of self keyword.

                var s = self
                	
                var a: Callable = func(): print(self)
                var b: Callable = func(): print(Node.new())
                var c: Callable = func(): print(s)
                	
                print(a.get_object())
                print(b.get_object())
                print(c.get_object())

                  xyz Thanks, callable c not being associated with self really makes the point clear. So only lambdas that use the keyword self, or implicitly use it by referencing a member without prefixing self., get auto-disconnected.

                  • xyz replied to this.

                    jumpingmechanic Yes, because only those lambdas store the object reference, and the object keeps them in its internal list of incoming connections along with connections to its regular methods.

                    This can also be verified. The following will print only one of the lambdas as object's incoming connection:

                    ready.connect(func(): pass)
                    ready.connect(func(): self)
                    for c in get_incoming_connections():
                    	if c.signal == ready:
                    		print(c.callable)

                    So when the object is about to be deleted, it goes through the list of its incoming connections and disconnects the callables found in that list from their corresponding signals.

                    You can also verify this by looking at the Object class destructor in engine's source code. From Object::~Object() in object.cpp.

                    // Disconnect signals that connect to this object.
                    while (connections.size()) {
                    	Connection c = connections.front()->get();
                    	Object *obj = c.callable.get_object();
                    	bool disconnected = false;
                    	if (likely(obj)) {
                    		disconnected = c.signal.get_object()->_disconnect(c.signal.get_name(), c.callable, true);
                    	}
                    	if (unlikely(!disconnected)) {
                    		// If the disconnect has failed, abandon the connection to avoid getting trapped in an infinite loop here.
                    		connections.pop_front();
                    	}
                    }

                    Note that it calls Callable::get_object():