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 await keyword #22947

Closed
jkb0o opened this issue Oct 11, 2018 · 12 comments
Closed

Add await keyword #22947

jkb0o opened this issue Oct 11, 2018 · 12 comments

Comments

@jkb0o
Copy link
Contributor

jkb0o commented Oct 11, 2018

It would be amazing if I could write var result = await method(arg) instead of var result = yield(method(arg), "completed") as it highly improves code experience with low cost (no back compat issues, not so difficult to implement, etc.). I assume async to be a be a unary expression like return with only FunctionState possible operand.

I'm a big fan of yield to signals and have a lot of code like

func failed():
  errors += 1
  if errors == 3:
    errors = 0
    level = max(0, level-1)
    yield(view.cat.remove(), "completed")
    generate()
    yield(view.grow(), "completed")
    yield(view.reset(), "completed")
  else:
    yield(view.soft_reset(), "completed")

This code is harder to write, harder to read then

func failed():
  errors += 1
  if errors == 3:
    errors = 0
    await view.cat.remove()
    level = max(0, level-1)
    generate()
    await view.grow()
    await view.reset()
  else:
    await view.soft_reset()

This is feature proposal/discussion but I'm already on the road of implementing this for local fork.

@LikeLakers2
Copy link
Contributor

LikeLakers2 commented Oct 11, 2018

I'm not sure I understand. yield() with a signal parameter is used for waiting for a signal to be emitted (such as waiting for text_changed in a text field), which seems like a different use case from just waiting for a function to complete. Could you explain how await would be useful in that sort of case?

@jkb0o
Copy link
Contributor Author

jkb0o commented Oct 11, 2018

This is about GDScript, not Python. And this is only about completed signal on FunctionState object. yield with Two parameters (object and signal) is used for awaiting that signal to be emitted before continuing an execution. Super common use case (at least for me) is coroutines (functions that contains some yields and executes over time). When you call coroutine it returns FunctionState object with only completed signal which emits when coroutine function finishes execution. So you can yield to this object/signal and wait when coroutine complete. You can even create complex chains of coroutines with deep call stack and implement dificult things keeping code simple. And only ugly thing here is a requirement to always write , “completed”) each time even it is absolutely clear what you suppose to do. Again. And again. And again.

I propose to add some sugar to improve code readability and experience, so:

  1. You need to type less (await some_corotine() instead of yield(some_corotine(), "completed"))
  2. It is easier to read - you always know that await some_corotine() returns execution (and result) when the function completes. You also have no any doubts if it is some other custom object with complete signal got returned from a function.
  3. Of cource, yield(coroutine(), "completed") stays as it is, so there is no back compatibility issues.
  4. You can extend codebase with autoloads and simplify any regular calls to await form, for example
    await Time.wait(3) instead of yield(get_tree().create_timer(3), "timeout"), you just need to implement Time singleton with wait method:
function wait(t):
  yield(get_tree().create_timer(t), "timeout")

@LikeLakers2
Copy link
Contributor

LikeLakers2 commented Oct 11, 2018

I actually meant to remove that reference to Python before submitting my comment, woops!

That being said, while I understand now, I'm not sure I can see this alias being useful enough in game code to warrant being added. I know I've never needed to wait for a FunctionState to emit completed (when I need to perform some action over a length of time, I use a Timer, or AnimationPlayer, or Tween, or even just another node that's a temporary child that performs the action) but perhaps others may have actual use cases (as you seem to).

Consider me neutral on the subject.

@OvermindDL1
Copy link

await itself really isn't that useful compared to what is available now until coroutines get added to GDScript, which is what await is built on in the great majority of languages (short of unbounded continuations and other such magic like that, plus proper coroutines are far more useful than just a simple await). I don't see the point in adding it yet as it will only be a half-measure at this stage.

@jkb0o
Copy link
Contributor Author

jkb0o commented Oct 12, 2018

@OvermindDL1 GDScript has coroutines already. Any function yielding for a signal is an implicit coroutine. It may be useful to define coroutines explicitly async func some_corotine(): so function becomes coroutine even without yeilds. It allows to avoid corner cases when functions conditionally become coroutine:

func get_frames_time(num_frames:int):
  var time = 0
  while num_frames > 0:
    time += get_process_delta_time()
    num_frames -= 1
    yield(get_tree(), "idle_frame")
  return time

func _ready():
  # I suggest to use
  # var time = await get_frames_time(x)
  var ten_frames = yield(get_frames_time(10), "completed") # its ok, everything goes as expected
  var zero_frames = yield(get_frames_time(0), "completed") # error here, get_frames_time(0) returns 0, can't yield to it

async func get_frames_time(): will not raise error becouse it is always coroutine independent from args passed.

But async is out of the scope of the current proposal, and even without it, GDScript has amazing coroutines and await is highly usable.

@OvermindDL1
Copy link

OvermindDL1 commented Oct 12, 2018

GDScript has coroutines already.

GDScript has scoped coroutines, more similar to Python generators or function-bound coroutines like in C# or recent javascript's or so forth. Unbounded continuations are significantly more useful and powerful, though difficult to implement efficiently. If unbounded coroutines do get implemented then you'd be able to implement a full Effects system within the GDScript language itself though. (Effects as in Algebraic Effects, not game effects. With algebraic effects you could not only implement something like async from entirely within the language itself but also a vast amount of other useful constructs)

@jkb0o
Copy link
Contributor Author

jkb0o commented Oct 12, 2018

@OvermindDL1 why?

@OvermindDL1
Copy link

@JJay Hmm? Why what?

@ghost
Copy link

ghost commented Oct 13, 2018

'await func()' seems like a fine idea for shortening the typing and improving readability for end use.

As long as it works as expected, I certainly wouldn't mind having it around.

@LeonardMeagher2
Copy link
Contributor

can await handle functions that don't yield also?

This is what I currently do

var result = some_func()
if typeof(result ) == TYPE_OBJECT and result is GDScriptFunctionState:
    result = yield(result ,"completed")

the reason is I have functions that are only a coroutine if some function executing inside them are coroutines and it'd be nice to just be able to do

var result = await some_func() and not have to worry about if it's a coroutine or not, since it could be.

@ghost
Copy link

ghost commented Nov 28, 2018

@LeonardMeagher2 I've run into this problem too, when the the yield is conditional. Not sure if there is a clean or easy way to return a function state without either yielding at the start of the function, or doing what you have in your example. It may need its own issue, since this one is mainly aimed at just creating an alias for how it already works.

@clayjohn
Copy link
Member

Note: this is being closed as part of our move to GIP, but it is being implemented very soon in #39093. Accordingly, there is no need to reopen a new proposal. :)

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