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

Lambda notation for anonymous funcrefs #27718

Closed
jabcross opened this issue Apr 6, 2019 · 7 comments
Closed

Lambda notation for anonymous funcrefs #27718

jabcross opened this issue Apr 6, 2019 · 7 comments

Comments

@jabcross
Copy link
Contributor

jabcross commented Apr 6, 2019

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

@vnen
Copy link
Member

vnen commented Apr 18, 2019

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

@jahd2602
Copy link
Contributor

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.

@jahd2602
Copy link
Contributor

I find this part a bit confusing:

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

@aaronfranke
Copy link
Member

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?

@jahd2602
Copy link
Contributor

jahd2602 commented Apr 22, 2019

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

@mnn
Copy link

mnn commented Nov 2, 2019

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.

#17268 (comment)

@mhilbrunner
Copy link
Member

Looks like we're gettting first class functions with Godot 4.0 :)
#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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants