moonscript
moonscript copied to clipboard
Class methods' implicit self is very confusing in generated Lua
I just spent about too long trying to figure out how the code works.
class SpecServer
current_server: nil
load_test_server: (overrides) =>
import get_free_port from require "lapis.cmd.util"
app_port = get_free_port!
more_config = { port: app_port }
if overrides
for k,v in pairs overrides
more_config[k] = v
@current_server = @runner\attach_server TEST_ENV, more_config
@current_server.app_port = app_port
@current_server
local SpecServer
do
local _class_0
local _base_0 = {
current_server = nil,
load_test_server = function(self, overrides)
local get_free_port
get_free_port = require("lapis.cmd.util").get_free_port
local app_port = get_free_port()
local more_config = {
port = app_port
}
if overrides then
for k, v in pairs(overrides) do
more_config[k] = v
end
end
self.current_server = self.runner:attach_server(TEST_ENV, more_config)
self.current_server.app_port = app_port
return self.current_server
end,
As we can see in the above, the => in moonscript is effectively the same as : in Lua, with the implicit self. This is fine. However, looking through more code we see the following:
use_test_server = function()
local setup, teardown
do
local _obj_0 = require("busted")
setup, teardown = _obj_0.setup, _obj_0.teardown
end
local load_test_server, close_test_server
do
local _obj_0 = require("lapis.spec.server")
load_test_server, close_test_server = _obj_0.load_test_server, _obj_0.close_test_server
end
setup(function()
return load_test_server()
end)
teardown(function()
return close_test_server()
end)
return use_db_connection()
end
Wait what? We're calling load_test_server without any args? what about that self? Shouldn't this be _obj_0:load_test_server()? How does calling a local function with no args magically know about the table it would otherwise be attached to?
local default_server = SpecServer()
return {
SpecServer = SpecServer,
load_test_server = (function()
local _base_0 = default_server
local _fn_0 = _base_0.load_test_server
return function(...)
return _fn_0(_base_0, ...)
end
end)()
}
Oh. That function is being wrapped to insert the self implicitly from within the module itself. That's super confusing! This works fine for moonscript but it's kind of an anti-pattern in Lua, making it quite confusing to use this particular module in Lua. Attempting to use it without thoroughly studying the code is basically impossible. I don't necessarily have a good solution, but I will say that attempting to use some modules written in moonscript, in Lua, can be a real challenge.
typically modules are tables of functions and other things. Not classes. the load_test_server is a helper function that wraps a default instance of the spec server
there is no implicit self, if you scroll to the bottom of the file that class is defined in you'll see the bound function being created manually. It looks something like default_server\load_test_server (notice no invocation, so it creates bound function)
I am aware of how Moonscript seems to treat the \ operator, it's only my concern that it creates anti-pattern behaviour when trying to interface via Lua. Wrapping the main function inside of another function that injects one of the args, and then presenting that function with the same name is very confusing. Lua's expected syntax would be server:load_test_server() but because the module doesn't adhere to typical Lua idioms, we end up with what looks like a stateless call to an arbitrary function that is actually injecting state into said function.
The generated Lua code may be reasonably clever in doing what it needs to do to implement Moonscript efficiently. This seems like an acceptable compromise. Do you see yourself needing to debug this generated class code frequently?
The issue is more than just this one class. The issue is that using Moonscript modules in Lua causes Lua code to not act like Lua. Anyone reading my Lua code would be very confused about why I am calling what looks like an anonymous/local/stateless function because it makes no sense in Lua to do that. The generated Lua doesn't look or act like Lua so trying to use the module in Lua code is awkward and confusing. What this looks like from a Lua perspective is that the function call either does nothing, or it does something to global variables set elsewhere. Both of these views are incorrect, so it becomes a matter of sifting through generated Lua code to figure out what is actually going on. It's one thing to trust the docs, but if my code isn't working and I need to do any amount of debugging, it becomes a lot of debugging. If the generated Lua acted more like idiomatic Lua, it wouldn't be a big deal.
local server = require "lapis.spec.server"
describe("/api", function()
setup(function()
server.load_test_server()
end)
teardown(function()
server.close_test_server()
end)
end)
As you can see from the code above, that looks very questionable. If I don't understand what is happening under the hood, I am left to make a few assumptions. Those assumptions would be:
- The code is wrong and does nothing
- The code is acting on globals
It would never cross a Lua programmer's mind that the above code acts on state set inside the module, but isn't actually tied to the module. That's a really weird thing to have happen.