Luau Method Call Shortcutting

As a Roblox developer, it is currently too hard to utilize method functions in callbacks, and its expensive.

If Roblox is able to address this issue, it would improve my development experience because it would allow me to write much more readable code, and utilize method calls in callbacks more performantly.

Demonstration of the problem:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Services = ReplicatedStorage:WaitForChild("Services")
local CollectionService = require(Services:WaitForChild("CollectionService"))

local collector = workspace:WaitForChild("Collector")
collector.Touched:Connect(function(touchedPart)
	CollectionService:Collect(touchedPart)
end)

One partial solution:

collector.Touched:Connect(CollectionService.Collect)

This fixes all of the issues, but unfortunately, this conflicts with many people’s programming styles, and, its impossible to do this for Roblox objects, such as Instances, or vectors, and conflicts with most custom class implementations in lua due to unavoidable reliance on self.

Conclusion

Being able to make method calls more directly would resolve a lot of the issues above. Additionally, it would be able to greatly benefit performance in cases where its not possible to do a regular method call on something.

One possible solution

A possible solution could be to allow for this expression syntax, as an operator:
local methodFunction = object:MethodName

Caveats tl;dr: Likely requires a new datatype which shortcuts a method call (e.g. method like vector), otherwise it would create ambiguity in its behaviour that is incompatible with existing code (e.g. (a:b)() vs a:b() having different call stacks)

This should be compatible with luau’s method optimizations to some extent at least, and has the flexibility to allow for some optimizations that aren’t currently possible, so I explored it a bit with a friend.

Some caveats

  1. This would likely need to return a new datatype or function, meaning object:MethodName == object.MethodName could be false. However, in all situations this would apply, this is already the case.
  2. This creates ambiguity for method calls (E.g. object:MethodName() becomes ambiguous… Is object:MethodName() equivalent to how it currently is, or does it behave as (object:MethodName)()? One way eliminates ambiguity, the other way ensures consistently (but might have a performance cost). The two would have to behave indistinguishably if separate from eachother.
  3. If a new function gets returned which invokes the method call, should it appear in the call stack? To preserve the example behaviour this would have to be the case, but this requires the previous caveat to separate the behaviour of both.
  4. If a new function does not get returned, a new datatype would likely need to be used which “shortcuts” a method call. (E.g. a method type, like vector)

It’s also good to note that the exact example behaviour this replaces isn’t at all important to preserve such as in the third caveat (and may actually not offer any benefit to preserve anyways), which would simultaneously solve the second caveat (but would require a new data type)

13 Likes

I feel like this will just cause more pain down the line and have serious compatibility issues, you already pointed them out. I think that for this specific use case they should instead look into something like arrow functions instead, for example in typescript (roblox-ts) it would be as sweet as collector.Touched.Connect(part => CollectionService.Collect(part)), that way a footgun is not added to the language. There have been feature requests for arrow functions, but the proposed syntaxes are not necessarily very sweet.

Or, you can just make your function return another function just for the sake of this.

7 Likes

Yeah. That would absolutely help with readability, but would still require wrapping functions in functions and I dislike the performance cost of that a ton. I just wish it could be more lightweight and more readable at the same time.

1 Like

I think this is an issue of trying to adapt the best practices to your programming style instead of adapting your style to the best practices. This is very easily avoided by not using : where it doesn’t need to be, or managing state in your code a different way.

Ultimately, there will always be cases where a middle man closure is required, as this is not an issue exclusive to Lua and is likely a lot more readable than a new operator that does an implicit wrapping.

3 Likes

Edit: I cleaned up my post a bit more to make it more clear about what I’m asking for in this feature request. @Autterfly @sjr04

I gave a few reasons for why I feel like this isn’t always an option in my post, mainly that there are a lot of cases where its not so easy to just swap : to .. I use a lot of class modules in my bigger projects, and all of the functions on the class are shared with a metatable, to conserve memory, and improve performance (which makes a huge difference).

The problem is, all of my class objects require the self argument. I can’t just swap : to . and move on, because any functions I’d like to use in this way need to have an object linked to them. This is the same for Instances, and other Roblox types, its simply not possible to swap to . instead of : because they rely on self.

You might just tell me to do this: instance.Function(instance, ...). The problem with this is, it usually ends up looking quite confusing in code, and this still doesn’t resolve the issue of connecting to events since events arguments are completely fixed.

I agree with you here, this is an issue that’s present in a lot of other programming languages and is pretty widespread. And, its not something that is well solved and I recognize the area I explored is not a great solution.

However, I would argue that the operator would be a lot more readable in these cases (maybe not in my specific example):

pcall(instance.Destroy, instance)
-- vs
pcall(instance:Destroy)
object[methodName](object, ...)
-- vs
objectMethods[methodName](...)
-- (Where objectMethods is a table of method values)
local someFunction = object.Function
local function someCall(func, object, ...) -- Requires another argument
	-- Stuff
	return func(object, arg1, arg2)
end
someCall(someFunction, object, ...)
-- vs
local someMethod = object:Function
local function someCall(method, ...)
	-- Stuff
	return method(arg1, arg2)
end
someCall(someMethod, ...)

Additionally, a datatype like this could be implemented to forward method calls directly. This isn’t really a benefit specific to the data type, but, being able to make direct method calls would be extremely useful.

Method calls are faster than a regular call to the method, particularly on Instances. You could “cache” methods and call them later hundreds of thousands of times without an extra performance cost. Abstractly speaking, if I could arbitrarily make direct method calls I could greatly speed up the performance of many pieces of code I’ve written without doing much work at all.

3 Likes

This can be done entirely in user-space if desired:

function bind(obj, fun)
    return function(...) return fun(obj, ...) end
end

-- alternative if you don't want to type object name twice
-- function bind(obj, method)
--    local fun = obj[method]
--    return function(...) return fun(obj, ...) end
-- end


part.Touched:Connect(bind(CollectionService, CollectionService.Collect))

We would need to allocate an object anyhow to store the method/object pair, and it’s unlikely that there’s a lot of performance to be gained there - in general what we found is that there’s a delicate balance between making function calling more intelligent and performant - for every call feature you add you have to carefully consider whether it adds a small slowdown to all function calls since these accumulate. So while it’s probable that a feature like this could be made a bit faster compared to the wrapper above, it also might make calls in general a bit slower, so we’d rather find more ways to make the calls in general a bit faster.

So with all that it feels pretty unlikely that we’d do that.

14 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.