godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.17k stars 98 forks source link

Add support for variadic functions (varargs) to GDScript #1034

Open MaaaxiKing opened 4 years ago

MaaaxiKing commented 4 years ago

Describe the project you are working on: Reaction game Describe the problem or limitation you are having in your project: I can't call a function with a variable amount of arguments (if they don't have a default)! Describe the feature / enhancement and how it helps to overcome the problem or limitation: I could call a function with a variable amount of arguments. Of course, you should also be able to pass the argument you want, not necessarily in the given order but this is something different: look here Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:

func my_func(*argv):  
    for arg in argv:  
        print (arg) 

my_func("Hello", "Welcome", "to", "Godot")  

If this enhancement will not be used often, can it be worked around with a few lines of script?: No Is there a reason why this should be core and not an add-on in the asset library?: Yes, it is useful for every project.

Bugsquad edit (keywords for easier searching): python, args, kwargs

Calinou commented 4 years ago

See also https://github.com/godotengine/godot/issues/16565.

Describe the project you are working on:

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

You should fill in those fields as well :slightly_smiling_face:


In the meantime, you can use this workaround. Not pretty, but it does the job with up to 9 arguments:

# Note that arguments explicitly passed as `null` will be ignored by this function.
func some_function(arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null, arg6 = null, arg7 = null, arg8 = null, arg9 = null):
    var array = []
    for argument in [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9]:
        if argument != null:
            array.push_back(argument)

    # Do stuff with `array`.
MaaaxiKing commented 4 years ago

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

I don't really understand this question haha because everything would be better to be core than an asset, wouldn't it, or do I understand nothing for that matter??

itsjavi commented 4 years ago

array pack and unpack for arguments would be great, similar to Javascript / Node / PHP:

<?php

function example(...$items)
{
  foreach($items as $item){
     print_r($item);
  }
}

// Calling the function:
example(... ["a", "b", "c"]);
// or
example("a", "b", "c");

I think this would be more intuitive and you can name the packed argument however you want.

On GDScript my syntax proposal is this similar:

func example(...items):
  for item in items:
     print(item)

# Calling the function:
example(... ["a", "b", "c"]) 
# or
example("a", "b", "c")

IMHO using * or ** operators is not so obvious because that's used for multiplication, exponentiation, pointers, etc. in other languages. In C for example, it's used for pointer dereferencing. That can lead to confusion.

Triple dot ... feels more natural and I think it doesn't have collisions with other operators. Plus, easier to type.

MaaaxiKing commented 4 years ago

I don't know if it would be harder to implement kwargs if this exponentiation operator would be added too. What do you think? I hope they won't interrupt each other if both of them will get part of gdscript.

shayneoneill commented 4 years ago

Thumps up on this one, "splat" arguments make designing clean APIs much nicer, and allows for better metaprogramming, (Ie, "Get all thse parameters, do something funky with them and then pass them to this function", opening up various functional (ie partial applications, currying, etc) and OOP methodologies (think of how python can pass on *args ,**kwargs to ancestors). Its a supremely useful construct.

Re: core vs addons. Not sure how you implement language features as plugins lol.

me2beats commented 4 years ago

this would be a useful feature but this would require implementing packing/unpacking arrays first, no? Btw python uses tuples for that

tx350z commented 4 years ago

I've come to the point in my current project where varargs would greatly improve code structure and readability.

I'd prefer the '...' over use of splats to avoid confusion as mentioned in previous comments.

Speedphoenix commented 4 years ago

I would like to point out that this would greatly help some builtin components of Godot.

For example the Tween node has a method with this signature

bool interpolate_callback(object: Object, duration: float, callback: String, arg1: Variant = null, arg2: Variant = null, arg3: Variant = null, arg4: Variant = null, arg5: Variant = null)

With the following description:

Calls callback of object after duration. arg1-arg5 are arguments to be passed to the callback.

This limits the amount of individual arguments to 5 for the callback, and bloats the signature of interpolate_callback (8 arguments is quite a lot in my opinion)

With variadic arguments, the method signature could be reduced to

bool interpolate_callback(object: Object, duration: float, callback: String, ...args)
# Or:
bool interpolate_callback(object: Object, duration: float, callback: String, ...args: Variant[])

And allow for more than 5 arguments to the callback.

Calinou commented 4 years ago

@Speedphoenix That said, Tween is being rewritten to have less methods that take lots of parameters: https://github.com/godotengine/godot/pull/41794

Speedphoenix commented 4 years ago

That looks great.

The tween.tween_callback(callback, params) will use an array for params, so I guess it would mostly be syntactic sugar to make that variadic

AaronRecord commented 3 years ago

Maybe this is a silly question, but how is:

my_func(arg0, arg1, arg2)

Any better than:

my_func([arg0, arg1, arg2])

It seems to me like it only saves writing 2 characters in exchange for making GDScript more complicated. This comment makes a good point, but couldn't varargs just be removed from the API as well? I'm having a hard time understanding why varargs is useful.

YuriSizov commented 3 years ago

@LightningAA Well, typing (as in writing code) aside the main benefit would be parameter validation. An array is just and array, but multiple distinct parameters, even in a variadic function, can be validated at a signature level. This may be less important if you don't rely on the typing system.

AaronRecord commented 3 years ago

@pycbouh But what about typed arrays that were added to GDScript 2.0?

YuriSizov commented 3 years ago

I guess they can help. But as I've said, that's a reason aside from writing code. But coding is also important. If a variadic function has some arguments before the varargs, it may be nicer to write them seamlessly instead of consciously breaking off a set of arguments into an array:

my_func(param0, param1, vararg0, vararg1)

vs

my_func(param0, param1, [ vararg0, vararg1 ])

That's a benefit for the user of the function, and the developer of the function can still write sensible code by using some sort of ...rest syntax.

me2beats commented 3 years ago

@pycbouh But what about typed arrays that were added to GDScript 2.0?

Typed arrays only work when all items are of one type. Functions arguments usually have different types

YuriSizov commented 3 years ago

Typed arrays only work when all items are of one type. Functions arguments usually have different types

Yeah, but varargs would either be of the same type or of a generic type like Variant, so it's not that different from a typed array.

AaronRecord commented 3 years ago

I'm still not completely sold,

my_func(param0, param1, [vararg0, vararg1])

Looks fine to me. I wouldn't really care if varargs were added, especially if they weren't that complicated to implement, but it feels to me like adding unnecessary complexity, "There should be one-- and preferably only one --obvious way to [pass a variable amount of arguments to your function]."

YuriSizov commented 3 years ago

"There should be one-- and preferably only one --obvious way to [pass a variable amount of arguments to your function]."

I feel like this is putting that idea to extreme. You can argue that you don't need multiple arguments at all, just pass everything as an array. One way and all.

Variadic functions do not serve the same purpose as passing an array as a parameter. Passing an array is a generic operation, and variadic function tells something very specific to the user about the operation. I don't have a good example handy, I'm afraid, but that's the point of having specific syntax — to pass on additional context. I agree that maybe typed arrays may be a middle ground though.

MaaaxiKing commented 3 years ago

Looking at this topic almost exactly a year later, I do not agree anymore with myself concerning the syntax, because I learned Java and got to know how it is made there with the three dots, it does even have much more sense than a star!

AaronRecord commented 3 years ago

You can argue that you don't need multiple arguments at all, just pass everything as an array. One way and all.

Yes, but then all your arguments would have to be the same type or any type, and there'd be no assigning names to different parameters, and the amount of parameters would be hard to enforce at compile time. varargs is already pretty much already an array, and all it does is save the user typing 2 characters.

Passing an array is a generic operation, and variadic function tells something very specific to the user about the operation.

That's a better argument, but I'd still like to hear some examples 😄

matthew-salerno commented 3 years ago

I think the python syntax should be used as that's what gdscript draws most from. I don't particularly care about *args, but **kwargs would be a lifesaver. For example, let's look at this sklearn class for python:

sklearn.tree.DecisionTreeRegressor(*, criterion='mse', splitter='best', max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, ccp_alpha=0.0)

Most of the default values are fine, but you may want to change a few in the middle. In Godot, you'd either have to pass a dict and check and fill default values manually in the method, or enter in every argument's default value until you reach the one you want to change when calling. This is quite cumbersome. That being said, *args could help make things clearer when passing flags:

enum {flag1, flag2, flag3}
func myFunc(arg1, arg2, *args):
    if args.has(flag1):
        pass
    elif args.has(flag2):
        pass
    elif args.has(flag3):
        pass

it doesn't make a huge difference like kwargs, but it does make the code a little more readable. IMO, myfunc(value1, value2, flag3, flag1) is more recognizable than myfunc(value1, value2, [flag3, flag1]), though the latter isn't that bad.

AaronRecord commented 3 years ago

I think the python syntax should be used as that's what gdscript draws most from. I don't particularly care about *args, but **kwargs would be a lifesaver. For example, let's look at this sklearn class for python:

sklearn.tree.DecisionTreeRegressor(*, criterion='mse', splitter='best', max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, ccp_alpha=0.0)

Most of the default values are fine, but you may want to change a few in the middle. In Godot, you'd either have to pass a dict and check and fill default values manually in the method, or enter in every argument's default value until you reach the one you want to change when calling. This is quite cumbersome.

I'm not a python expert, but that sounds more like named arguements (https://github.com/godotengine/godot-proposals/issues/902), not *kwargs (which I understand to be like args but it's a dictionary instead of an array).

matthew-salerno commented 3 years ago

I think the python syntax should be used as that's what gdscript draws most from. I don't particularly care about *args, but **kwargs would be a lifesaver. For example, let's look at this sklearn class for python:

sklearn.tree.DecisionTreeRegressor(*, criterion='mse', splitter='best', max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, ccp_alpha=0.0)

Most of the default values are fine, but you may want to change a few in the middle. In Godot, you'd either have to pass a dict and check and fill default values manually in the method, or enter in every argument's default value until you reach the one you want to change when calling. This is quite cumbersome.

I'm not a python expert, but that sounds more like named arguements (#902), not *kwargs (which I understand to be like args but it's a dictionary instead of an array).

You are absolutely correct. My mistake. In addition, my point on flags would be better implemented with bit masks.

GGalizzi commented 3 years ago

I'm still not completely sold,

my_func(param0, param1, [vararg0, vararg1])

Looks fine to me. I wouldn't really care if varargs were added, especially if they weren't that complicated to implement, but it feels to me like adding unnecessary complexity, "There should be one-- and preferably only one --obvious way to [pass a variable amount of arguments to your function]."

My main problem with the lack of varargs right now, unless I'm missing something in Godot 4. Is that there already are internal functions that do use varargs, such as Callable.call, I can't use an array like you suggest, to create some function that wraps a Callable.call to call some previously assigned Callable with some assigned arguments.

And that seems like an oversight to me, if we already have internal functions that do use varargs, then I believe we at least definitely need a way to unpack an array into arguments.

arrisar commented 2 years ago

Has there been any news since November? I would love this as a feature. Many people have mentioned before, but there are already many features that use this, so being able to write our own is a no-brainer.

Calinou commented 2 years ago

Has there been any news since November? I would love this as a feature. Many people have mentioned before, but there are already many features that use this, so being able to write our own is a no-brainer.

There are no contributors available to implement this feature, so this is unlikely to be implemented for 4.0.

rguca commented 2 years ago
callable.get_object().callv(callable.get_method(), args)

Workaround for Callable

blipk commented 2 years ago

I'm trying to do something like below, which would be a lot easier if I could override prints and call it with .prints as well as having variadic arg access so I'm not passing a single array to .prints, which causes the output to be encapsulated in string quotation or brackets/braces for other objects. Also, I'm not sure if it changes how the elements of the array are displayed as opposed to passing them as seperate arguments to builtin prints()

export var DEBUG_LEVEL = 1
func log(args, debug_level = DEBUG_LEVEL, no_repeat = false):
    if debug_level > 0:
        if no_repeat and var2str(args) == var2str(_last_log):
            _last_log = args
            return
        prints(args)
        _last_log = args

Language elements like this are useful for reflection which can improve developer usage.

EDIT: Managed to get something more predictable using a funcref:

if typeof(args) != TYPE_ARRAY:
    args = [args]
var p = funcref(self, "prints")
p.call_funcv(args)

EDIT2: Actually that doesn't work, I guess because prints() isn't on self. I tried funcref(GDScript, "prints") but that didn't work either

blipk commented 2 years ago
callable.get_object().callv(callable.get_method(), args)

Workaround for Callable

This is a useful addition to 4.0 and the new GDScript but still you can't create a callable from anything in @GlobalScope or @GDScript I tried the following but obviously it won't work as @GlobalScope is an annotation and not an identifier: var callable = Callable(@GlobalScope, "prints")

filipworksdev commented 2 years ago

I was instructed to post my solution here.

My proposal is to add a locally scoped parameters or alternatively attributes or arguments variable that is available only inside the function and which contains all the parameters passed to that specific function. This is similar to how JavaScript historically handled this.

Note that parameters in this case does not exist outside the scope of a function. I think this could also be used with a Callable call() or a lambda function.

def add():
  sum = 0
  for x in parameters: # any passed parameters automatically get collected in locally scoped parameters property
     sum += x
  return sum

def _ready():
  print( add(1,2,3,4,5,6,7,8) ) # parameters for this function call would be  [1,2,3,4,5,6,7,8]
  print( add(1,2,3) ) # parameters for this function call would be [1,2,3]

Named parameters would reduce the amount of values in parameters local variable.

def print_params(a,b):
  for x in parameters:
     print(x)

def _ready(a,b):
  print_params(1,2,3,4,5,6,7,8)  # a = 1 b = 2 parameters = [3,4,5,6,7,8]
  print_params(1,2,3)  # a = 1 b = 2 parameters = [3]
tx350z commented 2 years ago

My proposal is to add a locally scoped parameters or alternatively attributes or arguments variable that is available only inside the function and which contains all the parameters passed to that specific function. This is similar to how JavaScript historically handled this.

Note that parameters in this case does not exist outside the scope of a function. I think this could also be used with a Callable call() or a lambda function.

I like the idea of having an "automatic" variable to hold the variable parameters. When trying to described a couple tweaks to the idea I realized it is not clear (indicative) that a method accepts variable parameters. Adding support for the ellipses (...) notation in the method signature clearly indicates the method that accepts a variable number of parameters and should make auto-completion much easier. It would also be nice if the automatic variable containing the varargs has a name that is otherwise not allowed for defined variables (@@varargs, ~~varargs, $$args?) and is easy for the parser to recognize without confusion. This assures there is no accidental collision with developer defined variable names.

bsil78 commented 2 years ago

In order to be more formal about varargs, maybe an Array in last position could be seen as varargs in a call by GDScript ?

Example :

func myFun(a:int,others:Array): [...]

myFun(1,"b",2,Vector.ZERO) is understood then as myFun(1, ["b",2,Vector.ZERO)

People could argue they want to keep arguments control of analyser doing its job, thus some annotation to activate it, like @ Varargs, added in front of method could be a solution :

@ Varargs func myFun(a:int,others:Array): [...]

bsil78 commented 2 years ago

I've come to the point in my current project where varargs would greatly improve code structure and readability.

I'd prefer the '...' over use of splats to avoid confusion as mentioned in previous comments.

func myFun(a:int, ...others): [...]

=> would implies others is Array, that seems good too and safer that annotation I proposed, because it is on argument itself

I think it has to be reserved to last parameter to be more simple to parse and check.

arrisar commented 2 years ago

In order to be more formal about varargs, maybe an Array in last position could be seen as varargs in a call by GDScript ?

Making an argument type conditionally collect all remaining args would likely be more prone to causing issues. I'm personally +1 for the spread operator to "collect" the args beyond that point, as is common in other language implementations.

If the language can imperatively interpret an array type as collecting the remaining args, then it can interpret a spread or other operator to declaratively do it instead.

That said, there's some merit to that suggestion also. If it works out simpler in implementation, the last arg could just as well be typed myFunction([...], varargs args).

coderbloke commented 1 year ago

Today I would also need this, and found this discussion. I lost a bit between the different suggestion. For who don't know this in Java, if it helps, they solved vararg this way:

It would look like this:

func print_sum(of_what: String, values: int...):
    var sum := 0
    for value in values:
        sum += value
    print("%s = %d" % [of_what, sum])

So its just an array. Built-in Array supports all Variant types, so can be translated. But you can use it like this:

var already_spent_money: int = get_it_from_somewhere()
var open_planned_costs: int = get_it_from_elsewhere()
print_sum("foreseen overall cost", already_spent_money, open_planned_costs)

But also like this:

var planned_cost_item: Array[int] = get_hundreds_of_int_from_eg_a_database()
print_sum("overall opened plan cost", planned_cost_items)

Could work without type hints:

func print_sum(of_what, values...):
    ...

And with default values:

func print_sum(of_what: String, values: int... = []):
    ...

If "..." does not look nice, with a varargs keyword its the same.

An as I remember, if zero number of varargs is given by the caller, the function simply gets an empty array (so not null, no need for null check inside the function). I cannot decide now, which one is more useful inside the function.

coderbloke commented 1 year ago

Ahh OK sorry, I see "..." was already in discussion

yunylz commented 1 year ago

this is really important!! please devs consider this...

AThousandShips commented 1 year ago

If someone wants to implement this they're free to do so, no decision needs to be made before doing so, and someone showing an implementation improves the chances of it being accepted, some of the core developers that have done large work on GDScript can take it on if they are interested and have ideas, but features are added when someone figures out how to implement them, and then approved, it's not a process of "okay, this is good, now we'll tell someone to go do it".

Thebas commented 1 year ago

Any good solution for this example?

func call_method(method):
    if is_online and is_multiplayer_authority(): 
        rpc(method)
    else:
        call(method)

was hoping for:

func call_method(method, varargs : ...Variant):
    if is_online and is_multiplayer_authority(): 
        rpc(method, varargs)
    else:
        call(method, varargs)

Tried to find a sugared solution but couldn't figure it out. Curious. Maybe this is a good example FOR varargs implementation. Thank you in advance.

vvvvvvitor commented 5 months ago

Any progress on this? Time and time again I run into the issue of needing varidic functions on my projects, at this point I might just make my own GDExtension to remedy the problem.

dalexeev commented 5 months ago

@vvvvvvitor See godotengine/godot#82808. This implements the first part of the proposal, rest parameter, which allows you to declare variadic functions (i.e. declare a parameter that packs extra arguments into an array). This part is quite small, I think it is completely ready.

However, many also expect the second part, spread syntax, which allows you to unpack arrays into argument/element lists. While this is essentially just syntactic sugar for callv() and array1 + array2, this is the more complex part. I started working on this, but paused when I ran into some problems with static analysis. I plan to continue working on this in the future.

In any case, this will not be included in 4.3 due to the feature freeze, neither the first nor the second part. As for 4.4, it depends on many factors (will the spread syntax part be ready, will other contributors have time to test and review it).

RpxdYTX commented 5 months ago

Any good solution for this example?

func call_method(method):
  if is_online and is_multiplayer_authority(): 
      rpc(method)
  else:
      call(method)

was hoping for:

func call_method(method, varargs : ...Variant):
  if is_online and is_multiplayer_authority(): 
      rpc(method, varargs)
  else:
      call(method, varargs)

Tried to find a sugared solution but couldn't figure it out. Curious. Maybe this is a good example FOR varargs implementation. Thank you in advance.

At least for now you could pass the additional method parameters as an array and call bindv on method:

func call_method(method, args = []):
    ...
        rpc(method.bindv(args))
    ... 

call_method(print, ["hello"])

or

func call_method(method): ...

call_method(print.bind("hello"))
call_method(print.bindv(["hello"]))
call_method(func(): print("hello"))