For those who don’t know what function overloading is, it’s changing the behaviour of a function usually based on argument types/ or even argument count.
Here’s a quick C++ example:
#include <iostream>
void test(int a, int b, int c) {
std::cout << a*b*c;
}
void test() {
std::cout << "No args passed\n";
}
int main() {
test(9, 8, 2); // outputs 144
test(); // outputs No args passed
}
It doesn’t redefine the function or anything, it just adds an overload. When called with three arguments here, it will multiply all three and output the result. When called with none, it outputs No args passed
Back to my question
Since you can’t overload functions the same way in Lua, I’m wondering what’s a decent way to go about it.
To replicate function overloading in Lua, since there’s no true overloading, you’d probably have to do type checks and stuff which I would like to avoid.
local function test(a, b, c)
if a == nil and b == nil and c == nil then
print("No args passed") -- let's just assume nil wasn't explicitly passed
return
end
print(a*b*c)
end
test() -- No args passed
test(9, 8, 2) -- 144
If you go to the api reference page for CFrame you will see that the constructor CFrame.new has six overloads. I’m curious how roblox does it. I imagine roblox doing this overloading in a smart way. Literally the longest one is 12 arguments, so how does roblox account for ones that take vectors, and ones that take less arguments?
Roblox functions are generally implemented in C++, so they can overload like you did in the first example. That’s almost definitely what they do with the CFrame library. Ignore this, see @Autterfly’s reply.
Edit: Rodux, a project on the official Roblox Github account, uses a type-checking system similar to your approach. The most obvious example of this is in their thunkMiddleware.lua file. This is a bit of an old project, but it doesn’t seem like they do anything particularly unusual.
local function thunkMiddleware(nextDispatch, store)
return function(action)
if typeof(action) == "function" then
return action(store)
else
return nextDispatch(action)
end
end
end
return thunkMiddleware
As posatta said, the overloading that you describe in the API was implemented through C++.
For the multiplication function you mentioned above, your best bet would be to either pass a table {1, 2, 3, + …} or accept a tuple parameter. Although, as you mentioned, this really only works if you know the types of the arguments you pass in. Since we aren’t strongly typed (yet), overloading function parameters like in c++ just doest exist.
You can overload arithmetic operators using metamethods. This is helpful if you are using metatables in some sort of OOP implementation.
A really hacky method is you could write a small library for doing this. (lol)
Here is a messy example that I didn’t test but probably illustrates the concept:
local Function = {}
function Function.new(...)
local groups = {...}
local calls = {}
for i, v in pairs(groups) do
calls[v[1]] = v[2]
end
local function new(...)
local args = {...}
local callback = calls[#args]
assert(callback, "Unexpected Variable Count")
return callback(...)
end
return new
end
local function test1(a, b, c)
return a * b * c
end
local function test2()
return "noo"
end
local specialFunction = Function.new({3, test1}, {0, test2})
print(specialFunction(1, 2, 3))
print(specialFunction())
Of course then you could add automatic type checking if you were to expand upon this example.
Of course this is pretty hacky and you probably do not want to use something like this. But for the time being it looks “nice” and works fine.
You will probably just want to do what @idkhowtocode referenced about accepting a tuple parameter.
local function a(...)
local args = {...}
if #args == 3 then
local a, b, c = unpack(args)
return a * b * c
elseif #args == 0 then
return "bad food"
end
end
I am thinking of making this into a constructor like
local function MutableVector3(v3) -- construct a MutableVector3 of a regular Vector3
but I would like to implement overloads:
MutableVector3() - identity
MutableVector3(x, y, z) - construct a new one out of given coordinates
Maybe not so useful but I’d still like to give it a go.
I guess I could do it based on argument count using select("#", ...) alone since no overloads here would use the same argument count but different types. But what if this is needed somewhere else?
With typed Lua in the works, I believe function overloading should be more possible.
This would defy the core aspect of Lua functions being a first class type. If you “overloaded” a function with 2 different types, and then tried attempting to set another variable to the function, it would be ambiguous as to which function you’re referring to. It’s much easier to just use argument count and types (as is internally done).
-- let's just assume this would actually work for overloads
local function test()
print("no argssss")
end
local function test(arg)
print(arg*7)
end
local test2 = test -- how would it be ambiguous if this is what you meant?
-- wouldn't it just follow the same rules since it points to same function?
-- orwould it be a specific overload?
Also @eLunate gave me a pretty clean solution
local overload_dispatch = {
function(arg1) -- fn/1
print('a', arg1);
return 1
end,
function(a1, a2) -- fn/2
print('b', a1, a2);
return 2
end
}
local function overloaded_fn(...)
local n = select('#',...);
return assert(overload_dispatch[n], "Bad arg count")(...)
end
In normal Lua, what you’re doing is creating a new instance of the function (a LClosure) and binding it to a variable.
local test; test = function() print('no args') end
local test; test = function(arg) print(arg * 7) end
This is essentially what you’re doing. There’s no “same rules” to follow unless you rewrite Lua function Closures to be able to hold multiple function definitions, which could add more confusion and overhead for the runtime type checking or argument number comparisons that have to take place.
It honestly adds a weird layer of indirection not desired by a scripting language.
Now that I think of it, if the syntax for overloading was the same it could be ambiguous to wanting to point to a new function. that’s probably what you were saying i probably missed it/didn’t interpret it like so
Guess I will base my overloads on argument count and not end up in a situation where I need the same count for different types!
Thank you!