A discussion on the GrayHack Discord server today got me thinking about how to invoke functions where you don't know what the parameters might be, or even how many there might be, when you're writing the code. For example, you might be looking the function up in a big map of functions based on user input, and each of those functions might need different arguments.
This can be done in MiniScript, though it's a bit of work to set up. The key is that the str
form of a function looks like "FUNCTION( parameters )", with the parameter names as they were when the function was originally defined. We can parse that out to return the parameters as a list, like so:
parameters = function(f)
s = str(@f)
return s[s.indexOf("(") + 1:s.indexOf(")")].split(", ")
end function
As an example, let's take this simple function:
testFunc = function(age, shoeSize, hairColor="brown")
print "You're " + age + " with " + hairColor +
" hair and size " + shoeSize + " shoes."
end function
If you call parameters(@testFunc)
, it returns ["age", "shoeSize", "hairColor=""brown"""]
. Each parameter has become an element in the list.
That's useful information, but how does it help us invoke a function with some number of arguments not known until runtime? Unfortunately there's no great way to do that; we have to just check the length of our argument list, and handle each case separately, for example:
if args.len == 0 then return f
if args.len == 1 then return f(args[0])
if args.len == 2 then return f(args[0], args[1])
if args.len == 3 then return f(args[0], args[1], args[2])
But of course we can wrap that ugliness up in a function. And while we're at it, let's make use of that parameters
function defined above to let you pass either an argument list, or an argument map, which specifies the value of each parameter by name. The neat thing about a map is that it doesn't matter what order you specify the parameters in, and any extra parameters the function doesn't need are simply ignored. Here's the complete invokeWithArgs
function to do all that:
invokeWithArgs = function(f, args)
if args isa map then
// build an argument list by matching parameter names
argList = []
for param in parameters(@f)
if param.indexOf("=") then param = param[:param.indexOf("=")]
argList.push args[param]
end for
args = argList
end if
if args.len == 0 then return f
if args.len == 1 then return f(args[0])
if args.len == 2 then return f(args[0], args[1])
if args.len == 3 then return f(args[0], args[1], args[2])
if args.len == 4 then return f(args[0], args[1], args[2], args[3])
if args.len == 5 then return f(args[0], args[1], args[2], args[3], args[4])
print "Too many args!"
end function
This supports up to 5 arguments; you might need to add more in your application (though if you find yourself using more than 10, you should probably rethink your function definition!). With this in hand, and using our testFunc from above, here's what it looks like in use:
invokeWithArgs @testFunc, [45, 10, "black"]
invokeWithArgs @testFunc, {"hairColor":"blue", "age":35, "shoeSize":12, "extra":42}
(Notice how the second example is specifying the parameters in a different order, and with an extra one that is ignored.) This produces the output:
You're 45 with black hair and size 10 shoes.
You're 35 with blue hair and size 12 shoes.
Neat, huh?