3
\$\begingroup\$

In Godot GDScript, I have a function that returns a float.

Inside the body of the function, I use the built-in method find, that can return -1 if nothing is found.

In that case, I could place a condition right before the loop and return the float -1.0 (I can't return null and it looks like there aren't exceptions, nor optionals, nor other common things).

func get_my_float(c) -> float:
    
    var idx = p.find(c)
    
    for i in range(0, idx):
        
        //...

    return my_float

But: is there any other thing I can do?

Does it make any sense to return a Variant, instead?

\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

You are correct in that you can't return null, which is because you have declared in your method that it returns a float. So, yes, removing the type declaration so it returns a variant is an option.

If you do this, remember that you should check null.

Be aware that operating with null will cause an error. But since this error happens when you try to use null it is away from the cause of the null. Which does not make for the most useful of errors.

Also the Godot editor will show green line numbers on the lines that are type-safe, and show gray line numbers otherwise (i.e. using variant or unsafe casts), which is useful when reviewing your code.


You could return NAN (not a number) which would allow you to keep the float type. With a couple caveats:

  • Consider if NAN might be a valid result in some cases.
  • Remember that you need to check for NAN. And nothing is equal to NAN, in fact one of the ways to check if a variable is NAN is checking if it is different to itself, or you can use the more readable is_nan(…) method.

Similarly to null, the errors that come from NAN happen away from the cause. However operating with NAN will result in more NAN. Which is arguably worse that using null. Thus, if you can help it, please don't use NAN.


You are also correct that Godot does not have exceptions. You might, however, use push_error and return. Which leads to a question: why would you use push_error?

If you come from language that has exception, you might be familiar with the idea that some exceptions are intended to help the developer. For example "argument out of range" exceptions. You should not catch those exceptions. Instead, you should fix the code so they don't come up in runtime. However, you still want it to show during testing and debugging. And, of course, if some of them sneak into production, you want them to show up in logs.

You get that with push_warning and push_error. They will show up in the debugger, and also in logs. And you cannot catch them like exceptions, because they aren't exceptions.

You might also consider using assert for this purpose, or you can use the breakpoint keyword, given that those will interrupt in the debugger.


Option types and similar approaches has been proposed, but - at the time of writing - have not been implemented.

You could have a custom return type. For example you can define a nested class (add them at the bottom of the same file) with a float and a bool.

In Godot 3.x:

class OptionalFloat:
    var has_value:bool
    var value:float setget set_value

    func set_value(mod_value:float) -> void:
        value = mod_value
        has_value = true

In Godot 4:

class OptionalFloat:
    var has_value:bool
    var value:float:
        set (mod_value):
            value = mod_value
            has_value = true

And then use it like this:

func get_my_float(c) -> OptionalFloat:
    var result := OptionalFloat.new()
    var idx := p.find(c)
    if idx == -1:
        return result
    
    for i in range(0, idx):
        # …
        result.value = something
        # …

    return result

The examples above do not handle the case where you try to consume the value without checking if there is any. You can define a getter alongside the setter to do that. And perhaps use push_error there.

Be aware that these optional types would also require some kind of check (similar to checking for null or checking for NAN).

OK, you could do that. In particular if you really want to keep the type information. However, it is not a common approach. Instead people often fallback to variant, since it can represent a float (or whatever type) or null, also has some overhead, but you have to write less.


So Godot does not have out parameters. In fact, you cannot choose to pass by reference. But you can pass an array!

func get_my_float(c, result:Array = []) -> void:
    var idx := p.find(c)
    
    for i in range(0, idx):
        # …
        result.append(something)
        # …

Notice here that result is optional (it has a default value).

In Godot 4 you could use a typed array (Array[float]).

That is the approach that we would use if we need the return value, and also we need to have some additional optional output. In fact, this approach is used in parts of Godot API.

Ah, but in this case we have left the result as void here. So we could use the return value instead. That is, we might return an array (which could be empty for no result):

func get_my_float(c) -> Array:
    var result := []
    var idx := p.find(c)
    if idx == -1:
        return result
 
    for i in range(0, idx):
        # …
        result.append(something)
        # …

    return result

Here, since we are returning, we could use packed arrays. In Godot 3 you can use a PoolRealArray. And in Godot 4, the equivalent of the PoolRealArray is either PackedFloat64Array or PackedFloat32Array (in Godot 3 which one you get depended on whether or not you had a 64 bits build or 32 bits build). And I point out we can use them when we are returning because these are value types.

Be aware that this does not eludes a check either, since you would have to check if the array is empty.


You might be putting the effort in the wrong place. If you make your function take the index in a parameter, how to handle when the value is not found becomes a problem of the layer above… There you have better context and could pick an appropriate action. So the method at hand only needs to handle the "argument out of range" situation.


Speaking of appropriate actions. I don't know what you are doing, because I lack said better context… But given the method name, an option that comes to mind is set a fallback default result:

func get_my_float(c, fallback := 0.0) -> float:
    var idx := p.find(c)
    if idx == -1:
        return fallback
    
    for i in range(0, idx):
        # …

    return my_float

Some examples of possible default values which might make sense depending on the context: 0.0, 1.0, INF, -INF, NAN.

Alternatively, consider if you should think of a get_or_add_my_float method. Or something like that.


Another approach you might consider is to pass continuation callbacks.

Godot 4 has the Callable type, inline anonymous methods ("lambda"), and you can pass methods as Callable directly.

In Godot 3 you can use the FuncRef type and the funcref method to create them.


You know better than me what the situation is. I have given you options, you decide.

\$\endgroup\$
11
  • 1
    \$\begingroup\$ @LifeafterGuest I take it as a boarding feature. If you are not yet familiar with types, or if you come from a language that uses full dynamic typing, then you can start writing GDScript with variant and it will work. And afterwards you can start adding type information steadily on your code base. Which is in the spirit of separation of concerns. Besides, there are areas of Godot that still lack typing (notably dictionaries), so you might still need to use variant in some places. With all that said, I agree that it is better to avoid variant when we can. \$\endgroup\$
    – Theraot
    Apr 3, 2023 at 15:04
  • 1
    \$\begingroup\$ Is returning NaN an option here? I expected to see that as one of the suggestions, but maybe there's a reason it's not available or inappropriate? \$\endgroup\$
    – DMGregory
    Apr 4, 2023 at 21:35
  • 1
    \$\begingroup\$ @DMGregory Sure, you could return NAN, I'll add it. I didn't consider NAN in particular, but I quickly discarding using some value to represent no value, because it might be one of the valid values (i.e. perhaps it should NAN in some valid cases). \$\endgroup\$
    – Theraot
    Apr 4, 2023 at 21:52
  • 1
    \$\begingroup\$ i'm not familiar with gdscript but there is also another option if there are pointers/references available; the function takes a reference/pointer to the variable and returns a bool; if the function's conditions are met, the passed-in reference is modified to the output value, and true is returned, otherwise no modification is done and false is returned. the user calls the function in an if statement and uses the modified variable(s) if if's check succeeds \$\endgroup\$ Apr 4, 2023 at 22:10
  • 2
    \$\begingroup\$ @TheWrecker As I mention in the answer, you cannot choose to pass by reference. You can work around it by passing an Array which I also mention in the answer. Although I didn't propose the equivalent of the particular solution you suggest. \$\endgroup\$
    – Theraot
    Apr 4, 2023 at 22:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .