Skip to content

Instantly share code, notes, and snippets.

@PrestonKnopp
Last active September 20, 2023 21:15
Show Gist options
  • Save PrestonKnopp/00f488d13d3d6eabc6a5fbdb3560c1af to your computer and use it in GitHub Desktop.
Save PrestonKnopp/00f488d13d3d6eabc6a5fbdb3560c1af to your computer and use it in GitHub Desktop.
Attempt at Optionals in GDScript
# optional.gd
#
# Caveat
# ------
# This only works with types inheriting from Object. Built-in types don't work
# with the call api. Built-ins can still be fetched but they cannot be operated
# on.
#
# This can be fixed by subclassing optional and adding type specific methods
# that wrap the builtin type.
#
# Usage
# -----
# var opt = Optional.new(val)
# match opt.kind():
# opt.SOME: return 'It Exists!'
# opt.NONE: return 'It null!'
#
# match opt.kind_value():
# [opt.SOME, var value]: print('Hello, ', value)
# [opt.NONE]: print('No Value')
#
# match opt.value():
# null: pass
# var value: print('Hello, ', value)
#
# if opt.has_some():
# opt.value().hello_world()
#
# if opt.has_none():
# print('Value is null')
#
# # will only set if opt has some
# opt.some_property_of_val = 'woooo'
# # will get value of hello or return 'default_val'
# var hello = opt.hello.value('default_val')
#
# # call func
# opt.do('some_func', arg1, arg2, ..., arg12).value()
#
# # operate on optionals
#
# # This allows you to build up a query and if any of the steps fail
# # will return none optional.
# opt.do('some_func').eq(this).do('other_func').pass_to(obj, 'func').value()
# # The above could also be written as
# opt.some_func.eq(this).do_func.pass_to(obj, 'func').value()
#
# # This will pass the value of a some optional to my_obj_func and get the
# # result of that function as an optional.
# opt.pass_to(my_obj, 'my_obj_func', [pre_args], [post_args])
#
# # funcs with no args can be called directly like accessing a property.
# # this will only work if there isn't a property of the same name. If there
# # is use the opt.do() func.
# opt.pass_to(my_obj, 'get_next').this_func.value()
extends Reference
# This is used as a unique identifier for invalid arguments to `do()`
class _N:
extends Reference
enum {
NONE,
SOME
}
var __value
func _init(nullable_value):
__value = nullable_value
func set_value(nullable_value):
"""
Set value to reuse this optional rather than create a new instance.
"""
__value = nullable_value
func make(nullable_value):
""" @override
Make new instance with `nullable_value`.
Override this method if want to do custom initialization or
use a pool of optionals to reduce memory usage.
Make can be overriden to set __value as `nullable_value` instead of
creating a new instance. Reference semantics could possibly make this
behavior confusing, but it would be more lightweight.
"""
return get_script().new(nullable_value)
func kind():
"""
Get the kind (SOME or NONE) that this Optional is representing.
"""
if has_none():
return NONE
return SOME
func kind_value():
"""
Get a kind and the value tuple. Intended usage is for match bind
expression.
"""
if has_none():
return [NONE]
return [SOME, value()]
func value(with_default=null):
"""
Get the value if this optional has some, otherwise get the default.
"""
if has_none():
return with_default
return __value
func has_some():
"""
Check if this optional has a non null value.
"""
return not has_none()
func has_none():
"""
Check if this optional has a null value.
"""
if typeof(__value) == TYPE_OBJECT:
return weakref(__value) == null
return __value == null
# ------------------------------------------------------------------------------
# Operations
# ------------------------------------------------------------------------------
func _set(key, value):
"""
Set the `value` for `key` of this optional.
@usage
opt.do('get_child', 9001).some_prop = 'whatwhat'
"""
# returning true will prevent invalid set error being raised
if has_none():
return true
__value.set(key, value)
return true
func _get(key):
"""
Get the value from `key`. If a property with key does not exist it will
attempt to call function with name `key`. If that fails it will throw an
error.
@usage
opt.some_prop
opt.some_func_call.some_prop
"""
if has_none():
return self
if key in __value:
return make(__value.get(key))
else:
return make(__value.call(key))
func do_v(func_name, args=[]):
"""
Simulate callv. @see do()
@usage
opt.do_v('func_name', [one, two])
"""
var first_NIL_idx = args.find(_N)
var a = args
# Remove _N args
if first_NIL_idx != -1:
args.resize(first_NIL_idx)
# INCOMING hack to workaround callv not failing with invalid func call
# e.g. calling Node.get_name(0) should error, but callv just returns null
var call_result
match args.size():
0: call_result = __value.call(func_name)
1: call_result = __value.call(func_name, a[0])
2: call_result = __value.call(func_name, a[0], a[1])
3: call_result = __value.call(func_name, a[0], a[1], a[2])
4: call_result = __value.call(func_name, a[0], a[1], a[2], a[3])
5: call_result = __value.call(func_name, a[0], a[1], a[2], a[3], a[4])
6: call_result = __value.call(func_name, a[0], a[1], a[2], a[3], a[4], a[5])
7: call_result = __value.call(func_name, a[0], a[1], a[2], a[3], a[4], a[5], a[6])
8: call_result = __value.call(func_name, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7])
9: call_result = __value.call(func_name, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8])
10: call_result = __value.call(func_name, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9])
11: call_result = __value.call(func_name, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10])
12: call_result = __value.call(func_name, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11])
return make(call_result)
func do(func_name, a1=_N, a2=_N, a3=_N, a4=_N, a5=_N, a6=_N, a7=_N, a8=_N, a9=_N, a10=_N, a11=_N, a12=_N):
"""
Call func `func_name` with arguments only when this has some value.
Returns and wraps the result of the call to `func_name`.
@usage
var n = Node.new()
n.name = 'whatwhat'
var opt = Optional.new(n)
opt.do('get_name') # some('whatwhat')
opt.get_name # some('whatwhat')
opt.name # some('whatwhat')
opt.do('get_child', 0) # none
"""
if has_none():
return self
return do_v(func_name, [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12])
func do_set(property, obj, func_name, args=[]):
"""
Set `proprety` with the result of `obj.func_name(args)`.
Only sets `property` and calls `func_name` when has some. Returns self.
@usage
Optional.new(john).residence.do_set('address', self, 'create_address')
"""
if has_none():
return self
# saves from creating a new Optional instance by caching value
var cache_value = __value
__value = obj
set(property, do_v(func_name, args).value())
__value = cache_value
return self
func eq(value):
"""
Check if some equals `value`. If true return self else return none.
@usage
opt.do('get_obj').some_prop.eq(expected).prop_of_some_prop = 'Whatever'
"""
if value == __value:
return self
return make(null)
func pass_to(obj, func_name, pre_args=[], post_args=[]):
"""
Pass some as an argument into `obj`.`func_name`() after `pre_args` and
before `post_args`.
@usage
var opt = Optional.new(two)
opt.pass_to(myobj, 'func_on_myobj', [one], [three])
"""
if has_none():
return self
return do_v(func_name, pre_args + [__value] + post_args)
# reuse_optional.gd
#
# Each operation does not instance a new optional. It will set value of itself
# to the new value.
#
# This is more lightweight but can be weird with ref semantics.
#
# var opt = Optional.new(val).some_prop
# opt == Optional(result of some_prop)
# var next_opt = opt.do('func', arg)
# next_opt == opt == Optional(result of func(arg))
extends 'optional.gd'
func make(nullable_value):
set_value(nullable_value)
return self
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment