godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.26k stars 101 forks source link

Add `Callable::bind_map()` method to allow arbitrary binds #12625

Open lodetrick opened 3 weeks ago

lodetrick commented 3 weeks ago

Describe the project you are working on

Godot :)

Describe the problem or limitation you are having in your project

See: #4920, #10533, #4878 Relevant: https://github.com/godotengine/godot-proposals/issues/4920#issuecomment-1516974551

Currently, there is no way to bind Callables from the left or in an arbitrary position. This would be useful for connecting signals to methods that are not necessarily in the same order, or for binding arguments to the beginning of functions.

A use case that is of personal interest is simplifying chaining signals in C++ core code, as this currently requires creating an in-between method as boilerplate.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Add a bind_map method to callable that takes in an array, and maps the arguments onto the array. This will hopefully solve current and future concerns with binding and unbinding variables to Callable, as it is more general than the current model of right-to-left (un)binding.

This method is more general than the bind_left and prebind suggested in previous proposals, as well as being a separate concept so there is no decision paralysis as was a concern.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

Add a bind_map method to callable that takes in an array, and maps the arguments onto the array.

# The callable would be created like this:
test_callable = foo.bind_map("Hello", BindArg.ARG_2, 42.34, BindArg.ARG_0)

# This would transform a call to test_callable like:
# IN:     test_callable.call("Argument 0 is used", "Argument 1 is not used", "Argument 2 is used")
# CALLED: foo("Hello", "Argument 2 is used", 42.34, "Argument 0 is used")

Passing values into bind_map is treated as binding them.

The BindArg enum allows for the arguments passed in to the callable to be used. The enum is specifically short (to write) in order to make this easy to read, as this function would have many uses of the enum at once.

If this enhancement will not be used often, can it be worked around with a few lines of script?

This can be worked around with a boilerplate method but there is already precedent for variable binding being within callable, and this extends that.

Is there a reason why this should be core and not an add-on in the asset library?

This proposal is about extending the functionality of a core class.

Lexyth commented 2 days ago

How about something along the lines of foo.fix_arg(0, "Hello").map_arg(1, 2).fix_arg(2, 42.34).map_arg(3, 0), where fix_arg(index: int, value: Variant) sets the arg at index to value, essentially ignoring the argument at that index in the calls and instead using the value, and map_arg(index_param: int, index_arg: int) maps the argument in the call at index_arg to index_param (the index in the parameter list of the function), overriding it and, therefore, like fix_arg, it ignores the argument at index_param in the call, and instead uses the argument at index_arg in the call.

It's more calls, but it avoids the limitation of enums (functions can take an arbitrary number of arguments), it's not much more verbose, it won't break compatability, doesn't induce decision paralysis, and allows overriding values at arbitrary places WITHOUT shifting arguments around or having to manually rearrange the arguments so they don't shift.

Also, I'm not sure the term 'bind' is adequate, as, at least for me, it is associated with shifting arguments around instead of replacing them.