Install Asset
Install via Godot
To maintain one source of truth, Godot Asset Library is just a mirror of the old asset library so you can download directly on Godot via the integrated asset library browser
Quick Information
This plugin uses code generation to simulate generics for a best-attempt at static Option/Result types. Long story short, some parts of the end result are still dynamic, but the plugin has proven useful to me as a drop-in solution for null annoyances. Every dynamic part has asserts to protect against headaches.A lot of the API surface won't make sense unless you read the README in the source repository. Also, you can find an example project there with a method chaining example, a pattern matching example, and unit tests.
Option/Result for Godot
This plugin enables absence-of-value semantics in Godot, removing the concept of null from a workflow. The API surface is basically copied from Rust's Option and Result enums, although I have never actually used Rust (shame).
Code generation is used to achieve this. All detected types, including built-in types, will have an Option and Result counter-part generated. For example, int will generate Oint and Rint. A class called ZombieEnemy will generate OZombieEnemy and RZombieEnemy.
Check out the included project for examples of the method chaining workaround, pattern matching, and unit tests for both Option and result land. Make sure to read the bullet points below. Check the bottom of this document for explanations on the method chaining workaround and pattern matching syntax.
What problem(s) does this solve?
- You call
my_tween.kill()andCannot access method 'kill' on type 'Nil'throws even though the object was statically aTween, notNil. - For some reason you want a
floatvariable that you can set as having no value, but0.0is actually a valid value. - You want to force yourself and others to handle a possibly
nullobject. - You want something more advanced than an
ifstatement for exhausting all cases of a possiblenullvalue.
What important things do I need to know?
- Because this plugin generates files, hundreds of files will inevitably be added to your project. The file management is contained, but this means an increase in project size (around 27.0 MB at ~800 types, meaning all built-in types).
- This is not a compile-time concept like Rust's Option/Result enum. The Option/Result (aka Opt/Res) classes in this plugin extend
RefCounted. - None of the types cross-reference each other by design which prevents the parser from exploding. Due to this, method chaining is replaced with hacky syntactic sugar. This workaround is entirely dynamic and not static at all. Many dynamic asserts are included for safety with useful messages. See below.
- Pattern matching is not done through the
matchkeyword. Incorrectly typed lambdas are caught during runtime by Godot as per usual. Dynamic asserts are included for safety with useful messages. See below. The semantics are kind of similar to Rust, in that you can match Option(s) to Some and None, but you cannot match against expressions directly in the cases. - Converting between Option and Result land will return either
OVariantorRVariantdue to the conversion being inherently dynamic. This value should be cast one way or another. - There is no way to explicitly generate only certain types, because I that is not functionality I want to implement. I want this plugin to be a lazy drop-in solution with no configuration.
Is this actually useful and production ready?
- I developed this as a drop-in solution to the tween example above. It immediately solved the issue and felt clean doing so.
- This is not production ready and no testing has been performed other than API unit tests and me having fun.
- There are seemingly no runtime performance issues other than the obvious boxing of your optional values.
Getting Started
- Copy the plugin folder into your
res://addons/folder. - Click
Project → Tools → Option<T> & Result<T> → Create Core Files (res://optres/core/*) - Click
Project → Tools → Option<T> & Result<T> → Generate Personas (res://optres/persona/*) - Expect a crash or a very long script parse. The script parse will only occur once.
- Restart the editor if you want to make sure the one-time script parse actually occurred.
All generated files are contained within res://optres/ and can be deleted without affecting the plugin directly.
Generating files for new types you create is as simple as repeating step three. This will not recreate all types, only new types that aren't generated yet.
Pattern Matching
var msg: String = Oint\
.match(might_be_int, might_be_int_2, might_be_int_3)\
.to(&"String").do(
func ssn (a: int, b: int) -> String:
return ("First value is %s, second is %s, and third is none.") % [a, b],
func sns (a: int, c: int) -> String:
return ("First value is %s, second is none, and third is %s.") % [a, c],
func sss (a: int, b: int, c: int) -> String:
return ("First value is %s, second is %s, and third is %s.") % [a, b, c],
func __s (c: int) -> String:
return "At least third value was Some.",
func () -> String:
return "Option defaulted.",
)
Explanation:
Oint.match(...opts: Array[Oint])is a static, variadic method used to pattern match one or moreOintinstances..to(type: StringName | GDScript)is an optional method for the developer to assert the type the match must evaluate to.- All built-in types are passed as
StringNamei.e..to(&"Node3D"). - All custom types are passed directly using their keyword to support easier refactoring, i.e.
.to(MyCustomClass).
- All built-in types are passed as
.do(...cases: Array[Callable])accepts a list of one or more lambdas that represent cases. The name of the lamda is how you define the case. This method is variadic.func ssn (a: int, b: int)matches a case where the first and secondOintare Some, but the third is None.func sns (a: int, c: int)does the same except matching the first and third as Some. Note that the argument list is in order of left-to-right based on the original list ofOintarguments provided to the match method.func __s (c: int)matches that the thirdOintis Some, but the first two can be either Some or None.func ()is the default case.
Note: there is no fall-through.
Method Chaining (the hacky workaround for it)
var applied_dmg: Oint = Chain.to(Oint).do(
Chain.start(base_dmg),
Oint.map_to_c(_apply_strength_bonus),
Oint.map_to_c(_apply_armor_reduction),
Oint.map_to_c(_apply_critical_hit),
Oint.map_to_c(func (dmg: int) -> int:
return dmg + _get_weakness_bonus(enemy_weakness),
),
)
Explanation (be prepared for hacky stuff):
Chain.do(..._values: Array[Variant])is a static, variadic method accepting a list of values. These values are not used by the method. The purpose of this method is to provide a controlled environment where the "chained version" of Opt/Res methods are executed. Chained versions of methods are suffixed with_c. My intention here is to provide a logical scope the developer can easliy parse as a human, in which they feel like they are "chaining" methods, even though it is not literal method chaining. Due to this process being inherently dynamic, as much dynamically asserted safety protections as possible were implemented. Chained versions of methods assert that they are receiving the correct type from the previous method..docan assert the type of the final value returned using.to. Chained versions of methods assert that the chain must first be initialized withChain.start. See below.Chain.start(value: Variant)is a static method which initializes the current chain with a starting value. As mentioned earlier, the next method in the chain will expect a certain type, and this method is included in that check..to(type: StringName | GDScript)is an optional method for the developer to assert the type the chain must evaluate to.- All built-in types are passed as
StringNamei.e..to(&"Node3D"). - All custom types are passed directly using their keyword to support easier refactoring, i.e.
.to(MyCustomClass).
- All built-in types are passed as
Other Notable Things
- The methods
zip_with_to_c,map_to,map_to_c,map_or_to,map_or_to_c,map_or_else_to,map_or_else_to_c,and_then_to,and_then_to_c,and_to,and_to_care suffixed with_tobecause of the behavior difference from their original counterpart. Because generated types cannot cross-reference each other, I had to decide to make the input tomapvariant or the output, for example. I chose to make the input variant and the output static, as this leads to the safest behavior, as the lambda can statically type the input. Anyways, these methods are static—not instance methods.Oint.map_tomeans you are mapping to anOint. Check the project example because it is hard to explain. OUnit,RUnit,OVariant, andRVariantare included as special types.OUnitcontains a possible backing value of typeUnit, which is only accessible as a singleton instance viaUnit.instance. This is used to create an Option that simulates a bool, if that is for some reason desired.OVariantcontains a possible backing value of typeVariant. This is implemented as a workaround for the constraint that Option and Result types in this plugin cannot actually convert to other types in a static way, due to the lack of generic types in GDScript. This is used when converting between Option and Result land.
This plugin uses code generation to simulate generics for a best-attempt at static Option/Result types. Long story short, some parts of the end result are still dynamic, but the plugin has proven useful to me as a drop-in solution for null annoyances. Every dynamic part has asserts to protect against headaches.
A lot of the API surface won't make sense unless you read the README in the source repository. Also, you can find an example project there with a method chaining example, a pattern matching example, and unit tests.
Reviews
Quick Information
This plugin uses code generation to simulate generics for a best-attempt at static Option/Result types. Long story short, some parts of the end result are still dynamic, but the plugin has proven useful to me as a drop-in solution for null annoyances. Every dynamic part has asserts to protect against headaches.A lot of the API surface won't make sense unless you read the README in the source repository. Also, you can find an example project there with a method chaining example, a pattern matching example, and unit tests.