What follows are some thoughts on what I’d like from scripting in studio. I don’t think this idea is good enough to be included as-is to ROBLOX, but it’s something I’d like so I wanted to share it and hear other’s thoughts.
A new import
function
I try to keep my code very modular, and split it into many (nested) ModuleScripts.
This inevitably leads to a big block of code at the top of every script that looks like this:
local Rectangle = require(script.Parent.Parent:WaitForChild("Geometry"):WaitForChild("Rectangle"))
local Approximator = require(script.Parent.Parent:WaitForChild("Terrain"):WaitForChild("Approximator"))
local Roughness = require(script.Parent.Parent:WaitForChild("Terrain"):WaitForChild("Roughness"))
local Union = require(script.Parent.Parent:WaitForChild("Geometry"):WaitForChild("Union"))
local FastMap = require(script.Parent.Parent:WaitForChild("Data"):WaitForChild("FastMap"))
local Fence = require(script:WaitForChild("Fence"))
local Tree = require(script:WaitForChild("Tree"))
Here is my proposal to improve the ergonomics of this:
local Rectangle = import "Geometry.Rectangle"
local Approximator = import "Terrain.Approximator"
local Roughness = import "Terrain.Roughness"
local Union = import "Geometry.Union"
local FastMap = import "Data.FastMap"
local Fence = import "Zones.Forest.Fence"
local Tree = import "Zones.Forest.Tree"
Several things would be going on here.
First, there would be two new services: ClientLibraries
and ServerLibraries
. The import
function searches these services for the given module name. A LocalScript searches ClientLibraries
only (ServerLibraries
is not replicated).
Some optional features of import
:
- use
import "Folder.*"
to get a dictionary that imported all of the children -
import
can either prefer the name inServerLibraries
or error if there is a conflict -
import
automatically:WaitForChild
(to some extent) to allow safely loading modules on e.g., LocalScripts on a bad connection
Lazy loading for recursive imports
One additional feature that I’d like would be lazy loading. This would allow modules to recursively refer to each other.
The object import
returns would be an empty table; the ModuleScript itself is not run upon import
ing it. When you access the metatable for the first time, (i.e., upon __index
; other uses like __newindex
, __add
, etc should probably assert
) the ModuleScript is run, and the keys/values returned are copied into this object.
This would apply some restrictions to what these “library” modules can do:
- can only return plain tables (not userdata, strings, numbers, or functions)
- tables with some kinds of metatables couldn’t be returned (just
__index
would be OK I think) - cannot yield in the “definition” of modules (outside any functions)
- still cannot refer recursively to other modules outside of any functions (though now this can be detected and trigger a descriptive error!)
If you’re not familiar with what I mean by modules recursively referring to each other:
-- this does NOT work
-- ModuleScript A --------------------------------------------------------------
local B = require(script.Parent.B)
function f(x)
B.f(Vector3.new(x / 2, x / 2, x / 2))
end
return {f = f}
-- ModuleScript B --------------------------------------------------------------
local A = require(script.Parent.A) -- no good:
-- we have to wait for A to finish running,
-- but A is waiting on B to finish running.
function f(size)
if size.magnitude > 10 then
A.f(size.x - 11)
A.f(size.x - 12)
else
local p = Instance.new("Part", workspace)
p.Size = x
end
end
return {f = f}
One solution to this problem is to just move the require
into every function:
function f(size)
local A = require(script.Parent.A) -- OKAY!
-- A has already finished since this doesn't block B's object from being returned
if size.magnitude > 10 then
A.f(size.x - 11)
A.f(size.x - 12)
else
local p = Instance.new("Part", workspace)
p.Size = x
end
end
but is kind of ugly. import
could simply do that delaying for us.
How it’s implemented
Here’s basically what the implementation of import
might look like
-- locate the given module in the given
local function findModule(module, parent)
assert(typeof(parent) == "Instance")
assert(type(name) == "string")
for name in module:gmatch("[^.]+") do -- XXX: accepts invalid formats
parent = parent:WaitForChild(name)
end
assert(parent:IsA("ModuleScript")) -- TODO: allow `.*` for folders
return parent
end
function import(name)
local module = findModule(name, game:GetService("ClientLibaries"))
or findModule(name, game:GetService("ServerLibraries"))
local context = nil
return setmetatable({}, {
__index = function(_, key)
if context then
return context[key]
else
context = require(module)
assert(context ~= nil)
return context[key]
end
end,
__newindex = function() error("libraries cannot be changed", 2) end,
__add = function() error("cannot added library", 2) end,
__sub = function() error("cannot added library", 2) end,
__pow = function() error("cannot added library", 2) end,
-- etc...
})
end
Yes, this could be written by myself and just included in every script, but that is exactly the problem, because right now ModuleScripts incur quite a bit of boilerplate.