moonscript icon indicating copy to clipboard operation
moonscript copied to clipboard

Class methods' implicit self is very confusing in generated Lua

Open karai17 opened this issue 6 years ago • 5 comments

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.

karai17 avatar Feb 02 '19 14:02 karai17

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)

leafo avatar Feb 03 '19 05:02 leafo

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.

karai17 avatar Feb 03 '19 13:02 karai17

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?

strait avatar May 22 '19 01:05 strait

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.

karai17 avatar May 22 '19 02:05 karai17

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:

  1. The code is wrong and does nothing
  2. 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.

karai17 avatar May 22 '19 02:05 karai17