-
-
Notifications
You must be signed in to change notification settings - Fork 216
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
Would current approach to _ex
methods lower QOL?
#328
Comments
one issue that has been discussed before with solution 1, is that godot adding a new default argument to a function becomes a breaking change for us. whereas it isn't a breaking change for godot. |
That is the case for sure, but I don't think it would happen frequently. And by the way, if there is a breaking change, meaning a new decision made in the background by the new API for the developer, I would prefer to face it as a broken piece of code, rather than an unexpected behavior in runtime. |
Thanks a lot for bringing this up! 🙂
Note that this is precisely the behavior that GDScript and C++ offers, and the whole point of default parameters: you don't have to provide values for them. But let's look into details more closely 😉
Is the problem missing documentation? If yes, that's something that will be addressed sooner or later. gdnative already has documentation for engine classes and methods. Popups happen in any modern IDE as well, the
Unfortunately, we have to live with the fact that Rust has neither named nor default parameters. I don't like the builder pattern either, in Kotlin we could write much nicer code. But it is what it is, and from all the possibilities, builders seemed the most sane and idiomatic (there was extensive discussion in godot-rust/gdnative#814). Commit 01ce0bb is also a good example how code in general gets more readable with #322. Have you seen the diff? I agree that in some cases the
That's something we could have a look at. I've never seen "do" in builder APIs though, "build" or "done" seems more frequent. But maybe that's related to Have you seen such Rust APIs anywhere?
The thing is, adding a default parameter is not a breaking change in C++ and GDScript, but it is in Rust -- at least if we go with the "full" method syntax. We can definitely not expect this to happen rarely just because our language is annoying in this regard. It also has very practical implications: once we are on crates.io, any such change (added default parameter) means we have to release a semver-breaking version. If we don't detect it, we violate versioning guarantees.
New default parameters should of course be added in a way where the default preserves the old behavior. But that's API design on Godot's part -- I'm confident that they regard this, but I cannot uphold it. The same is true for any breaking behavior in existing functionality, you don't need to add parameters if you want to break user code relying on certain assumptions. As such, I don't see this as an argument against the current system. |
Hi @Bromeon
Yes, sure. But shall we consider Rust as just another GDScript or C++? Developers can use GDScript with much less hassle, so what is the point of GDExtension? Performance is not the answer because in many cases, people use another language mainly because of its different ergonomics. Rust is different, and the way we use it may be as well. Rust does not have
Not really. Documentation is just the last resort. The real issue is ergonomics.
Many features of Rust that may seem like quirks, are actually things some people love about it.
Not only I have seen that, I've already change 29 modules of my own project and continued my work.
As you mentioned above correctly, "[Nice stuff] happen in any modern IDE as well". Please note that I'm not against extender approach, I think it is better not to be forced, and can be a helper. Nothing stops us from choosing to use the
This is when we may need
You're right. Double checked and it is a reserved keyword, so that's the part we can forget about.
The only true question here is: do we benefit in terms of ergonomics? I appreciate the great job you and the GDExt contributors have done. |
Putting them in one line, the first one is by far shorter, and still has the hints.
I'm always in favor of deliberate decision-making, awareness and control of details. In case of
I sure agree that using One other point that comes to mind is that there should be a level of coherency between
Your solution does it well, where priority is ease of use: // base case -- with defaults that we are expected to be aware of
let normal = scene_tree.create_timer(secs);
// extended case -- user must provide all
let deliberate = scene_tree.create_timer_ex(secs, false, false, false); Few things are about it to be addressed though:
The other solution is still towards the same basic goal you mentioned, but tries to address those objectives as well, by turning the question upside-down. The priority here is awareness & control while in a level it also keeps the freedom of setting parameters that exist in the // base case -- user must provide all (so must think consequences ahead)
let normal = scene_tree.create_timer(secs, false, false, false);
// optional -- user knowingly opts in to use defaults
// (and have the power to add one or two params when desired)
let deliberate = scene_tree.create_timer_ex(secs).done();
Thanks again for your efforts. p.s. |
I would like to note that One possible solution to the api-compatibility and semver issue is that we could add an edition-like system. So each gdext library will declare an "edition", probably matching a minor version of godot, like say 4.1. And if they declare that then gdext will always generate an api that matches what the api looked like in 4.1. If godot now adds a default argument to a function in 4.2, then we just wont let the user pass that default argument in through the normal function call, and it will always be the default. instead they'll need to either use the extender, or change their edition to 4.2. This should prevent any semver breaking changes that aren't semver breaking in godot, since the api will always match what a library was made for. Though we should probably require people to specify the edition, maybe something like (actually this exact syntax probably wont work): struct MyExtension;
#[gdextension(edition = 4.1)]
unsafe impl ExtensionLibrary for MyExtension {} So now we can have what @mhgolkar is suggesting with the default function having all parameters, required and default. In addition to the extender. Without fear of breakage. |
I'm still not sure if I fully understand this desire of control. Previously you also mentioned safety (which I understood as robustness against errors, not Yes, it's good to be in control -- but default parameters are an explicit "you don't have to care" feature. None of the above "notifies" you during library updates that the API has changed, because your existing code has no reason to stop working. You may or may not use the additional features, but there is no harm if you don't. We don't break user code if Godot adds new APIs, so I don't really see why we should do it when optional parameters are added? Adding an enum variant on the other hand is not necessarily forward-compatible -- a new case is introduced, which existing code doesn't handle. In cases where this is acceptable (aka by design),
This is a great observation. Somehow the in/out principle reminds me of variance and covertibility (Kotlin has a good explanation). It will be hard to classify enums in an automated way though -- and if Godot starts using enums in return positions, we would again be facing breakage. |
huh, you're right. that is exactly what this is. That is, say we have two enums A and B, such that every variant of A is also a variant of B. then we can say that A is a subtype of B. (of course rust doesn't really have subtyping but anyway) Then we know that a function that takes an input of type B can be treated as a function that takes an input of type A (contravariance, since and if a function returns a value of type A, we can treat it as a function that returns a value of type B (covariance, since So now if we have an enum which has an extra variant added to it, this means the old enum is a subtype of the new one. So we can safely update any function that takes this enum as an input to the new enum, since those functions are subtypes of the old functions. but we can't safely update any function that returns it as an output since the new function is a supertype of the old one. (or equivalently, if an enum is only ever used as an input, it's not breaking to update. but if it's used as a return type, then an update is breaking. this is obviously ignoring all the other aspects to what makes something a breaking change.) |
I find everything above reasonable and agree. Just a few thoughts:
I don't understand it sometimes too! Maybe it's just a philosophical obsession.
Yes. You're correct. I edited that to "awareness and control", trying to dodge technical baggage of
The whole point is to make sure there will be some kind of (Rust) barrier (even artificial) notifying or forcing reaction, when changes happen. In case of all-provided arguments with optional extender, when the API (and probably my GDExtension edition) changes, then my code won't compile because I haven't provided a newly added required argument. It is all unless I've deliberately chosen to use extender and accept the defaults, which means my code feels nothing.
New API (method) is something you may use or not. New parameter/argument is a potential new behavior for the method you may have already used. Although in an ideal world (and thanks to all the cautious opensource developers out there in our not-so-ideal world) it is most-likely harmless, but there is no guarantee, specially in micro level.
I couldn't explain it any better! |
This may be possible in the specific case of default arguments, but is generally doomed to fail. Instead of add_child(child)
add_child_with(child, optional_arg) In C++ or C#, they could even use an overload: add_child(child)
add_child(child, optional_arg) The 2nd method is functionally identical to a default parameter. Making one artificially breaking while the other is forward-compatible looks arbitrary to me. Don't get me wrong -- I understand the desire to be notified about forward-compatible changes. But forcing it into the API is the wrong approach in my opinion. It's also technically impossible in most cases apart from default parameters. You should consider alternatives that are more promising, e.g.:
As mentioned earlier, Godot developers can also change semantics without adding a default parameter. This is no reason to revisit every single function you use. |
Well... I don't have any other argument to offer (and everything was under a question mark from the beginning), so you can close this issue if we've reached the conclusion. Thanks a lot for your time. |
Thanks a lot for the interesting discussion, I really enjoyed it! 👍 Even if the API is probably going to stay for the time being, there were some really good inputs regarding compatibility, ergonomics, enum variance and generally different trade-offs, some of which may be reconsidered in the future. Thanks a lot! |
Problem?
I'm not sure if this is a legitimate concern, so there is a question mark on top of everything,
but there are things about #332 that feel capable of lowering Quality-of-Life:
First thing that occurs to me, is the amount of decisions made on behalf of developers via default methods (without
_ex
).Some of these decisions are normal, but others can really make debugging a relatively large project a little problematic.
Here is an example:
Previously if a developer wanted to create a
SceneTreeTimer
, they had to decide for every single parameter.In a quick-and-dirty way it would look like this:
This is pretty much deliberate and educated, yet of-course a little longer.
Now with the ease provided by #332, they can go with:
The method makes few decisions for us (i.e. defaults):
Here is the catch/concern:
The parameters
process_always
,process_in_physics
andignore_time_scale
which can easily affect your game (e.g. in case of pausing the scene-tree) are only available in the time of initialization. TheSceneTreeTimer
does not expose them as properties or set/get methods. Forgetting this simple fact, a developer may easily get confused, why their timer does not respect pausing the scene-tree and gives them no way to change it!They should have done this:
Godot reminds us of these decisions being made (almost constantly, via popups in its editor or language server docs/hints), but in case of Rust-GDExt, we (at least for now) don't have access to that.
Specially in case of short methods, we are going to have some weird syntax.
Before #322 we had:
Now we have:
Suggestion
If this is actually something of a legitimate concern, and not just my taste,
I would suggest following approach(es) to address it:
I'm not opposed to #332 totally. The
extender
is a nice idea actually when you want it;but does it need to be forced?
If it is possible regarding internals of GDExt, one way could be to leave the main (
_ex
less) methods to behave as they used to.In that case developers could choose if they want to go with the defaults or set everything deliberately.
That would hint educated decisions, may actually seem cleaner in some cases, and solves the above-mentioned syntax weirdness.
And yes, I suggest
.do()
instead of.done()
as well, to protect our hands from typing fatigue.We can follow the Godot's approach and make sure all the defaulted methods (without
_ex
) remind the developer of the decisions made on behalf of them. We can have method level documentations pointing to the defaults. Albeit it does not solve the issue with weird and needlessly long syntax mentioned above.The text was updated successfully, but these errors were encountered: