-
-
Notifications
You must be signed in to change notification settings - Fork 68
Tutorial: Automatic Record Methods
Sometimes it's useful to define methods automatically via meta-programming, based on its first usage, this recipe will show how to do this in Nelua.
Note that this recipe requires some familiarity with Nelua meta-programming first.
People familiar with Lua know it's possible to do this via the __index
metamethod
and with Ruby know this is possible with the method_missing
idiom.
In Nelua this is also possible to do similar at compile-time; that is, you can define new methods based on its name on its first usage and there won't be runtime costs, because this will be done at compile-time.
This is useful, for example, to implement auto swizzling methods on math vector types, a common idiom used to code GPU shaders (what swizzling methods are will be described shortly), but they can make code with lots of math vector algebra more readable and simple.
This recipe will ultimately present an implementation for swizzling with some meta-programming.
To begin simple, let's define our math vector types:
local vec2: type = @record{x: number, y: number}
local vec3: type = @record{x: number, y: number, z: number}
local vec4: type = @record{x: number, y: number, z: number, w: number}
local v: vec4 = {x=1, y=2, z=3, w=4}
print(v.x, v.y, v.z, v.w) -- outputs: 1.0 2.0 3.0 4.0
A swizzling method in this case would be a method that converts a vec
type into other vec
type,
optionally changing the elements order; for example, let's implement one manually:
-- Swizzling method that returns a vec3 with elements `z`, `y` and `x`.
function vec4.zyx(self: vec4): vec3
return vec3{x=self.z, y=self.y, z=self.x}
end
local v: vec4 = {x=1, y=2, z=3, w=4}
local rv3: vec3 = v:zyx() -- a vec3 with the first 3 elements of `v` in reverse order
print(rv3.x, rv3.y, rv3.z) -- outputs: 3.0 2.0 1.0
Now suppose you want to implement all swizzling methods combinations for all vec
types manually;
that would be about 4*4*4*4 + 4*4*4 + 4*4 = 336
different methods for vec4
,
3*3*3*3 + 3*3*3 + 3*3 = 117
different methods for vec3
,
and 2*2*2*2 + 2*2*2 + 2*2 = 28
different methods for vec2
, in total 481 methods!
That is a lot of methods to implement manually and still a lot of methods to implement even via simple code generation of all combinations, because most of them will probably never be used, and this would put additional needless work in our compiler, thus increasing compile time.
Enter automatic method generation!
The idea is simple; what if we have a way to implement vec4.zyx
automatically on its first usage?
Well this is possible with some meta-programming; if we hook the first time
the compiler tries to index .zyx
method and we define the method right away
before the compiler uses it:
##[[
local skip = false
setmetatable(vec4.value.metafields, {__index = function(metafields, name)
if skip then return end -- avoid recursive calling `__index`
if not name:match('^[xyzw]+$') then return end -- filter unwanted method names
skip = true
print('defining method '..name) -- print index attempt (for debug purposes)
]]
function vec4:#|name|#() -- defined the method
print(#['in method '..name]#)
end
##[[
skip = false
return rawget(metafields, name)
end})
]]
local v: vec4 = {x=1, y=2, z=3, w=4}
v:xxx()
v:xxx()
The above example will produce the following output:
defining method 'xxx'
in method xxx
in method xxx
The defining method 'xxx'
is a message shown only at compile-time, for debug purposes;
note that it's only shown once, so we are really defining that method just once.
And the in method xxx
is a message shown twice, because we called the method xxx
two times in our test.
Great!
With this basic covered, now we only need to come up with some macros to implement what we desire; here it's the full working example of automatically defining swizzling methods:
-- Macro that automatically implements swizzling methods for math vec types.
##[[
local function vec_swizzling_methods(vecT)
static_assert(traits.is_symbol(vecT), 'expected a symbol!')
vecT = vecT.value -- get the symbol holded type
static_assert(traits.is_type(vecT), 'expected a symbol to a type!')
local skip = false -- used to avoid infinite lookup recursion
local function auto_swizzling_callback(metafields, name)
if skip then return end -- avoid recursive calling `__index`
if not name:match('^[xyzw]+$') then return end -- filter unwanted method names
skip = true
]]
function #[vecT]#.#|name|#(self: #[vecT]#): auto <inline>
## if #name == 4 then
return vec4{x=self.#|name:sub(1,1)|#, y=self.#|name:sub(2,2)|#,
z=self.#|name:sub(3,3)|#, w=self.#|name:sub(4,4)|#};
## elseif #name == 3 then
return vec3{x=self.#|name:sub(1,1)|#, y=self.#|name:sub(2,2)|#,
z=self.#|name:sub(3,3)|#};
## elseif #name == 2 then
return vec2{x=self.#|name:sub(1,1)|#, y=self.#|name:sub(2,2)|#};
## end
end
##[[
skip = false
return rawget(metafields, name)
end
-- hygienize, so we see only symbols available at the time this macro is called
auto_swizzling_callback = hygienize(auto_swizzling_callback)
-- non existent methods will call auto_swizzling_callback
setmetatable(vecT.metafields, {__index = auto_swizzling_callback})
end
]]
local vec2: type = @record{x: number, y: number}
local vec3: type = @record{x: number, y: number, z: number}
local vec4: type = @record{x: number, y: number, z: number, w: number}
-- Implement swizzling methods for vec types.
## vec_swizzling_methods(vec4)
## vec_swizzling_methods(vec3)
## vec_swizzling_methods(vec2)
do -- Test swizzling
local v = vec4{x=1, y=2, z=3, w=4}
assert(v:xyx() == vec3{x=1, y=2, z=1})
assert(v:yz() == vec2{x=2, y=3})
assert(v:xw():yyyy() == vec4{x=4, y=4, z=4, w=4})
local rv = v:wzyx()
print('reverse v is')
print(rv.x, rv.y, rv.z, rv.w) -- outputs: 4.0 3.0 2.0 1.0
end
Note that methods xyx
, yz
, xw
, yyyy
are not manually defined;
they were defined using meta-programming, compile-time magic!
Swizzling for vec
types like the above is widely used in shader languages like GLSL and HLSL,
they are handy to make math intensive algorithms.
That is for this recipe; this can be used for other interesting stuff.
For example, it could be used to define methods that do different things based on their name,
like in Ruby world some libraries automatically implement methods like find_by_
or
find_all_by_
suffixed with a field name, to specialize different find functions based on the field name.