Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
MaaaxiKing opened this issue Jun 10, 2020 · 42 comments · May be fixed by godotengine/godot#82808
Open

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

MaaaxiKing opened this issue Jun 10, 2020 · 42 comments · May be fixed by godotengine/godot#82808

Comments

@MaaaxiKing
Copy link

MaaaxiKing commented Jun 10, 2020

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
Copy link
Member

Calinou commented Jun 11, 2020

See also godotengine/godot#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 🙂


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
Copy link
Author

MaaaxiKing commented Jun 11, 2020

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
Copy link

itsjavi commented Jun 14, 2020

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.

@Calinou Calinou changed the title Add *args and **kwargs like in Python Add support for variadic functions (varargs) to GDScript Jun 18, 2020
@MaaaxiKing
Copy link
Author

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
Copy link

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
Copy link

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

@tx350z
Copy link

tx350z commented Oct 5, 2020

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
Copy link

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
Copy link
Member

Calinou commented Oct 26, 2020

@Speedphoenix That said, Tween is being rewritten to have less methods that take lots of parameters: godotengine/godot#41794

@Speedphoenix
Copy link

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

rosshadden added a commit to acleverpun/factory-wars that referenced this issue Jan 30, 2021
…ents#emit`.

New emit method to be removed when [support for varargs](godotengine/godot-proposals#1034) lands.

Signed-off-by: Ross Hadden <[email protected]>
@AaronRecord
Copy link

AaronRecord commented Jun 14, 2021

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
Copy link
Contributor

YuriSizov commented Jun 14, 2021

@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
Copy link

AaronRecord commented Jun 14, 2021

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

@YuriSizov
Copy link
Contributor

YuriSizov commented Jun 14, 2021

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
Copy link

@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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

"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
Copy link
Author

MaaaxiKing commented Jun 14, 2021

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
Copy link

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
Copy link

matthew-salerno commented Jun 19, 2021

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
Copy link

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

@matthew-salerno
Copy link

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.

@rguca
Copy link

rguca commented Mar 11, 2022

callable.get_object().callv(callable.get_method(), args)

Workaround for Callable

@YuriSizov YuriSizov moved this to In Discussion in Godot Proposal Metaverse Jul 21, 2022
@YuriSizov YuriSizov moved this from In Discussion to Ready for Review in Godot Proposal Metaverse Jul 21, 2022
@blipk
Copy link

blipk commented Aug 9, 2022

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
Copy link

blipk commented Aug 13, 2022

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
Copy link

filipworksdev commented Nov 7, 2022

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
Copy link

tx350z commented Nov 7, 2022

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
Copy link

bsil78 commented Nov 28, 2022

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
Copy link

bsil78 commented Nov 28, 2022

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
Copy link

arrisar commented Dec 1, 2022

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
Copy link

coderbloke commented Jun 1, 2023

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:

  • "..." suffix after type name in method signature, and it is only allowed for the last argument
  • inside function body, this parameters are simply an array
  • when function called, you can pass an array as parameters, or use "," to separate the values
    So as just "..." would be shorthand for array + allowing the caller to use different notation

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
Copy link

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

@yunylz

This comment was marked as off-topic.

@AThousandShips
Copy link
Member

AThousandShips commented Jun 12, 2023

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
Copy link

Thebas commented Sep 25, 2023

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
Copy link

vvvvvvitor commented Jun 15, 2024

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
Copy link
Member

@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
Copy link

RpxdYTX commented Jun 16, 2024

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"))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Ready for Review
Development

Successfully merging a pull request may close this issue.