Skip to content

Instantly share code, notes, and snippets.

@willnationsdev
Last active June 9, 2024 22:29
Show Gist options
  • Save willnationsdev/46bd72b62ae5888ca63fbfc3b414f6af to your computer and use it in GitHub Desktop.
Save willnationsdev/46bd72b62ae5888ca63fbfc3b414f6af to your computer and use it in GitHub Desktop.
GDScript Result class with usage example.
# Example usage
static func example_connect_to_network() -> Result:
return Result.fail(ERR_CANT_CONNECT)
static func example_load_file(conn: ENetConnection) -> Error:
return ERR_FILE_CORRUPT
class ApiResult:
extends RefCounted
var http_code
var body := ""
var error := OK
var msg := ""
func _init(code = 200, res_body = "", e = null, m = null):
http_code = code
body = res_body
error = e
msg = m
static func example_try_transform_through_api(buffer) -> ApiResult:
return ApiResult.new(500, "", ERR_BUG, "Failed to suplex buffer.")
static func example() -> void:
# Calling context can continuously flow like a `try` block, even with an error.
# Not limited to Result-returning logic thanks to ease of lambda functions.
# Unlike exceptions...
# - keeps runtime engine simpler & faster.
# - does not interrupt code flow & thus improves "reasonability".
# - the type system clearly notifies you of when you are engaging in
# operations that invoke conditional logic (w/ even more clarity if had generics).
var rst := Result \
.ok() \
.then(example_connect_to_network) \
.then(example_load_file, true) \
.then(func(data: PackedByteArray):
var res = example_try_transform_through_api(data)
return Result.new(res.body, res.error, res.msg)) \
.then(func(data: PackedByteArray):
print(data.size()))
# Now to evaluate the aggregate results, similar to a
# `try` block followed by many `catch (SomeTypeOfException ex)` blocks.
if rst.error == OK:
# All attempted operations were successful!
# We also opted-out of executing subsequent operations (mostly).
print(rst.value)
else:
match rst.error:
ERR_CANT_CONNECT: pass
ERR_FILE_CORRUPT: pass
var other: pass
## A two-state faux "discriminated union" Result class.
## Similar to, say, a Result<obj, (int, obj)> in a FP language.
class_name Result
extends RefCounted
var value = null
var error: Error = OK
var error_data = null
func _init(value = null, error: Error = OK, error_data = null) -> void:
self.value = value
self.error = error
self.error_data = error_data
func then(on_success: Callable, convert_error := false) -> Result:
# Short-circuit if already have error, like an exception would.
if error != null:
return self
# Attempt subsequent operation.
# If we had generics, we could use static typing to guarantee a Result is returned.
# Instead, we have to add a type check.
# Results get merged into the calling instance while non-Results are adopted.
var ret = on_success.call(value) if on_success else value
if ret is Result:
if ret.error:
self.error = ret.error
self.error_data = ret.error_data
else:
self.value = ret.value
# Use this for Error-returning functions with side-effects where
# you want to continue going with the chain so long as OK is returned.
elif convert_error and ret != OK:
self.error = ret.error
self.error_data = ret.error_data
else:
self.value = ret
return self
# Optional exercise left for the reader.
# Can be useful if you want to slip in intermediate error handling.
func catch(on_error: Error) -> Result:
return Result.fail(on_error.call(null))
# Factory methods for usability.
## Create successful result with a specific value.
static func ok(value = null) -> Result:
return Result.new(value)
## Create specific error with optional custom data (focus is on the error)
static func fail(error: Error, data = null) -> Result:
return Result.new(null, error, data)
## Create generic error with custom data (focus is on returning the data, e.g. a custom error object)
static func fail_with(data) -> Result:
return Result.new(null, FAILED, data)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment