Godot-proposals: A way to make `yield(x)` return a value alongside the `GDScriptFunctionState`

Created on 8 Feb 2020  Â·  5Comments  Â·  Source: godotengine/godot-proposals

Not working on a project; opening the proposal here to branch off the discussion in here

In the ongoing discussion to add await, it was suggested that "completed" be added as an implied second parameter to yield(x), which would then be used instead of the new operator await.

This is a counter-proposal to add a Python-like functionality to yield instead, in addition to the new operator. As a bonus, it will allow easy usage of algebraic effects patterns.

Current pattern

When using yield() without parameters, we can use resume from the caller to send information into the callee. However, it's difficult to send information back to the caller in this back-and-forth manner.

This is the biggest reason why I haven't really ever used .resume() in practice. Usually, I'm more interested in getting information out of a function, not as much into it.

Nowadays, to pass information out from a concurrent coroutine, you need to pass some common container (like an array) into it by parameter, and use that as shared state. This is bug-prone and requires you to change the interface of the function when you decide that you need that channel of information.

The following code prints "ABCDEF", and illustrates a current way to do this.

func foo():
    var in_channel = [null]
    # foo --> bar, by parameter
    var state = bar("A", in_channel)

    # foo <-- bar, by shared state
    var from_bar = in_channel[0]
    print(from_bar) # "B"

    # foo --> bar, by .resume
    state = state.resume("C")

    # foo <-- bar, by shared state
    from_bar = in_channel[0]
    print(from_bar) # "D"

    # foo --> bar, by .resume
    # foo <-- bar, by return
    from_bar = state.resume("E")
    print(from_bar) # "F"


func bar(x, in_channel):
    print(x) # "A"
    in_channel[0] = "B"
    var from_foo = yield()
    var print(from_foo) # "C"
    in_channel[0] = "D"
    from_foo = yield()
    print(from_foo) # "E"
    return "F"

As you can see, using in_channel is clunky and feels wrong in terms of shared state.

My proposal

In Python, yield is used to send information out of the running generator function into the caller's context. This proposal aims to add a way to use yield more in line with what a Python programmer would expect.

My proposal is as follows: add a yielded_value property to GDScriptFunctionState that contains the value of x, if yield(x) was called with a single parameter (and null otherwise). This way, there is a two-way communication channel between caller and callee. This code does the same as the previous one, but hopefully in a more elegant manner.

func foo():
    # foo --> bar, by parameter
    var state = bar("A")

    # foo <-- bar, by yield()
    var from_bar = state.yielded_value
    print(from_bar) # "B"

    # foo --> bar, by .resume
    state = state.resume("C")

    # foo <-- bar, by yield()
    from_bar = state.yielded_value
    print(from_bar) # "D"

    # foo --> bar, by .resume
    # foo <-- bar, by return
    from_bar = state.resume("E")
    print(from_bar) # "F"


func bar(x):
    print(x) # "A"
    var from_foo = yield("B")
    var print(from_foo) # "C"
    from_foo = yield("D")
    print(from_foo) # "E"
    return "F"

This will not break compatibility with current use of yield, since you can't use it with a single parameter currently. I just think this is a much better idea than making "completed" implied, since then we don't make an already pretty confusing function call even more confusing.

Cleaning up the interface

As a further (compatibility-breaking) proposal, I suggest splitting the functionality of yield() so that the two-parameter version gets moved to await(). As such, we'd have a better-behaved function interface for both:

yield(value=null)

and

await(object, signal='completed')

No more counting the number of arguments in the parser.


I hope it's okay to break the question form in this case. If not, is there a proper place to make a proposal in this format? I don't really have a current project that would benefit from this, since I have avoided this pattern because of the lack of this functionality.

gdscript

Most helpful comment

I didn't actually read the proposal, but just to leave a note: I intend to break some stuff in GDScript for Godot 4 and the yield functionality is one of those. I'll share my proposals when I get back from my trip.

Summing up, we'll probably remove yield (maybe save it for actual generators in the future) and just use await with signals. It's likely that the GDScriptFunctionState object won't be accessible anymore. Since signals will be first-class in Godot for, we can do simply: await $Button.pressed. Function calls will also be allowed: await my_func().

All 5 comments

I didn't actually read the proposal, but just to leave a note: I intend to break some stuff in GDScript for Godot 4 and the yield functionality is one of those. I'll share my proposals when I get back from my trip.

Summing up, we'll probably remove yield (maybe save it for actual generators in the future) and just use await with signals. It's likely that the GDScriptFunctionState object won't be accessible anymore. Since signals will be first-class in Godot for, we can do simply: await $Button.pressed. Function calls will also be allowed: await my_func().

This proposal is for the argumentless functionality of yield(), which has a very different use-case from the 2-argument version. They could coexist with await without much overlap, I think.

I don't think we really need the argumentless yield as it is today. A lot of languages live by without any kind of continuation object and I don't think it's super useful in GDScript, which is meant to be simple.

Generators, OTOH, can be very useful. I'm not really planning to add them soon, but I do want to investigate how they could work in GDScript.

What about yielding for a frame? I use that one a ton.
I also use yield timeout a lot. I use these so much so I'd love simple wait_s(4) wait_f(20) functions or something like that, if yield will no longer be there.

Await will do the same as yield in these cases.

On Fri, 14 Feb 2020 at 19:48, Seel notifications@github.com wrote:

What about yielding for a frame? I use that one a ton.
I also use yield timeout a lot. I use these so much so I'd love simple
wait_s(4) wait_f(20) functions or something like that, if yield will no
longer be there.

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/godotengine/godot-proposals/issues/453?email_source=notifications&email_token=ABQ3BPIS2XSQAZKHJQTDFCLRC4NSDA5CNFSM4KRXPXU2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEL2WTDQ#issuecomment-586508686,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ABQ3BPIQ5MLI5PHJMO5PL7DRC4NSDANCNFSM4KRXPXUQ
.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Torguen picture Torguen  Â·  3Comments

arkology picture arkology  Â·  3Comments

Dorifor picture Dorifor  Â·  3Comments

wijat picture wijat  Â·  3Comments

WilliamTambellini picture WilliamTambellini  Â·  3Comments