Godot: Lambda notation for anonymous funcrefs

Created on 6 Apr 2019  路  7Comments  路  Source: godotengine/godot

Right now, the function a funcref (or a object-methodname pair) points to needs to be declared separately, which is inconvenient for small one-liners, especially in cases like connect().

This is a proposal for some syntax that would automate the creation of these one-off functions. Keep in mind that this does not entail having functions as first-class citizens (yet), just a compile-time nicety. The generated function would still exist and have a name in the bytecode.

This would also have the side-effect of allowing things like connect() and Tween.interpolate*() to receive funcrefs directly, in addition to object-methodname pairs.

My proposal is inspired by JavaScript's notation, rather than Python's (since : now has typing connotation):

() -> return_value
param -> return_expression * 2
(param1: String, param2: int) -> (x = return_statement)

Just like Python, multi-statement lambdas would not be allowed.

The variable context would be that of the closest class, so

var x: int = 20

class Baz:
    var x : int = 10
    var y : FuncRef

    func foo():
        y = ()->print(self.x)  # lambda expression starts in line 8, column 12

    func bar():
        y.call_func()  # prints 10

would compile to the same bytecode as something like

var x : int = 20

class Baz:
    var x : int = 10
    var y : FuncRef

    func foo():
        y = funcref(self,"__anonymous_function_8_12")

    func bar():
        y.call_func() # prints 10

    func __anonymous_function_8_12():
        print(self.x)

Some usage examples could be:

connect("signal_name", x -> baz(x*10) )

$Tween.interpolate_method( (x)->(y = x * x) ,0.0,1.0, ...<remainder of Tween's args>)

And it would fit nicely with map, reduce and filter, which have also been requested.

[1,2,3].map(x -> x*2) // returns [2,4,6]
[1,2,3].filter(x -> x%2==1) // returns [1,3]
[1,2,3].reduce((x,y) -> x + y) // returns 6
[1,2,3].reduce((x,y) -> x + y, 7) // returns 13

As a bonus, we could detect if any properties from self are being used and otherwise define the generated function as static (which also would require funcref support for static functions).

archived feature proposal gdscript

Most helpful comment

@aaronfranke In the example you mention, a lambda wouldn't be worth it. But in other cases such as:

  1. Passing callbacks to signals
  2. Working with async code
  3. Creating a chain of functions, where a function calls the next once it finishes (probably after some time)
  4. Using functional methods such as map, reduce and filter*
  5. and more...

...lambdas are worth it. Specially when a function doesn't need a function name.

(*) I am currently using these kind of functions in a Godot 3.1 project with funcrefs, and looks very hacky.

All 7 comments

My idea for this would be to allow multiline lambdas as well. Somewhat like this:

func _ready():
    print("okay")
    var lambda = ():
        print("inside")
        print("also inside")

    lambda()

    connect("toggled", x: print(x))

    [1,2,3].reduce((x, y): x + y, 7)

    [1,2,3].map((x: int):
        var temp = x * 2
        return pow(temp, temp)
    )

    var transform = x:
        return x * 2

    # Binding from external context (syntax can change)
    [1,2,3].map((x).(transform): return transform(x))

Might be a little trick to implement all the nuances with indentation, but should be doable.

@jabcross suggested replacing the : with -> for lambdas. I'm considering though I think it's a bit inconsistent, since indented blocks usually start after :.

My idea for this would be to allow multiline lambdas as well. Somewhat like this:

func _ready():
    print("okay")
    var lambda = ():
        print("inside")
        print("also inside")

    lambda()

    connect("toggled", x: print(x))

    [1,2,3].reduce((x, y): x + y, 7)

    [1,2,3].map((x: int):
        var temp = x * 2
        return pow(temp, temp)
    )

    var transform = x:
        return x * 2

I really like this idea, and I would make the parenthesis mandatory, like this:

func _ready():
    print("okay")
    var lambda = ():
        print("inside")
        print("also inside")

    lambda()

    connect("toggled", (x): print(x))

    [1,2,3].reduce((x, y): x + y, 7)

    [1,2,3].map((x: int):
        var temp = x * 2
        return pow(temp, temp)
    )

    var transform = (x):
        return x * 2

That way, every time one sees (...): means lambda without doubt.

I find this part a bit confusing:

    [1,2,3].map((x).(transform): return transform(x))

Why would I want to do this:

func _ready():
    print("okay")
    var lambda = ():
        print("inside")
        print("also inside")

    lambda()

Instead of this:

func lambda():
    print("inside")
    print("also inside")

func _ready():
    print("okay")
    lambda()

Isn't the latter more readable as it requires knowing fewer syntactic rules?

@aaronfranke In the example you mention, a lambda wouldn't be worth it. But in other cases such as:

  1. Passing callbacks to signals
  2. Working with async code
  3. Creating a chain of functions, where a function calls the next once it finishes (probably after some time)
  4. Using functional methods such as map, reduce and filter*
  5. and more...

...lambdas are worth it. Specially when a function doesn't need a function name.

(*) I am currently using these kind of functions in a Godot 3.1 project with funcrefs, and looks very hacky.

I have recently published an experimental library Golden Gadget where lambdas are simulated (not ideal - essentially eval under the hood) and for some functions they are directly usable:

# monsters is an array of objects
var res = G(monsters).filter("x => x.is_alive && x.hp < 10").map("x => x.name").val

It also has a shorthand for creating funcrefs of anonymous functions (without closures):

F("x => x.b").call_func({b = true}) # true

At least until lambdas are in GDScript this might be a good enough solution. Of course all depends on your use case - array wrapper isn't free and while lambdas are being cached, their performance will undoubtedly be worse than native func.

https://github.com/godotengine/godot/issues/17268#issuecomment-548822364

Looks like we're gettting first class functions with Godot 4.0 :)
https://github.com/godotengine/godot/pull/39093


Feature and improvement proposals for the Godot Engine are now being discussed and reviewed in a dedicated Godot Improvement Proposals (GIP) (godotengine/godot-proposals) issue tracker. The GIP tracker has a detailed issue template designed so that proposals include all the relevant information to start a productive discussion and help the community assess the validity of the proposal for the engine.

The main (godotengine/godot) tracker is now solely dedicated to bug reports and Pull Requests, enabling contributors to have a better focus on bug fixing work. Therefore, we are now closing all older feature proposals on the main issue tracker.

If you are interested in this feature proposal, please open a new proposal on the GIP tracker following the given issue template (after checking that it doesn't exist already). Be sure to reference this closed issue if it includes any relevant discussion (which you are also encouraged to summarize in the new proposal). Thanks in advance!

Was this page helpful?
0 / 5 - 0 ratings