Is it possible to replicate Ruby's method_missing in Lua?
Asked Answered
B

3

6

I'm fairly sure that in Lua, you can use a given metatable's __index, __newindex, and __call to roughly replicate Ruby's method_missing. And I somewhat have:

function method_missing(selfs, func)

    local meta = getmetatable(selfs)
    local f
    if meta then
        f = meta.__index
    else
        meta = {}
        f = rawget
    end
    meta.__index = function(self, name)
        local v = f(self, name)
        if v then
            return v
        end

        local metahack = {
            __call = function(self, ...)
                return func(selfs, name, ...)
            end
        }
        return setmetatable({}, metahack)
    end

    setmetatable(selfs, meta)
end

_G:method_missing(function(self, name, ...)
    if name=="test_print" then
        print("Oh my lord, it's method missing!", ...)
    end
end)

test_print("I like me some method_missing abuse!")

print(this_should_be_nil)

My problem is this: While the syntax is similar, and I can certainly use it to replicate the functionality, it introduces a breaking error. Every single variable that you use in the context of the table you apply a method_missing to is never nil, since I have to return an object that can be called in order to pass the buck of the potential call from the index function to an actual call.

i.e. After defining a global method_missing as above, attempting to call undefined method 'test_print' runs as expected, but the value of test_print when indexed is non-nil, and other methods/variables that aren't responded to, like this_should_be_nil are non-nil.

So is it possible to avoid this pitfall? Or can the syntax not be bent to support this modification without modifying the language source itself? I imagine the difficulty arises in how in Ruby, indexing and calling are analogous, whereas in Lua they are distinct.

Berhley answered 4/11, 2013 at 20:10 Comment(2)
Is there also a use-case or a class of use-cases you typically use this technique to solve? You should probably mention it in your question to avoid the XY problem. Maybe lua can provide a different approach for it.Swinge
Since __index is not called if the value exists in a table, the first part of your __index function has no use.Astraddle
V
3

You can avoid this problem by making nil value callable.
Unfortunatelly, this can be done only from host code (i.e., C program), not from Lua script.

Pascal code:

function set_metatable_for_any_value_function(L: Plua_State): Integer; cdecl;
begin   // set_metatable_for_any_value(any_value, mt)
   lua_setmetatable(L, -2);
   Result := 0;
end;

procedure Test_Proc;
   var
      L: Plua_State;
   const
      Script =
'set_metatable_for_any_value(nil,                                        ' +
' {                                                                      ' +
'   __call = function()                                                  ' +
'              print "This method is under construction"                 ' +
'            end                                                         ' +
' }                                                                      ' +
')                                                                       ' +
'print(nonexisting_method == nil)                                        ' +
'nonexisting_method()                                                    ';
begin
   L := luaL_newstate;
   luaL_openlibs(L);
   lua_pushcfunction(L, lua_CFunction(@set_metatable_for_any_value_function));
   lua_setglobal(L, 'set_metatable_for_any_value');
   luaL_dostring(L, Script);
   lua_close(L);
end;

Output:

true
This method is under construction
Valet answered 4/11, 2013 at 22:57 Comment(4)
Aha! I believe that will be enough to let it function as expected, though there may be some unintended consequences...Berhley
@WesleyWigham This seems like syntax sugar. Why not just use rawget to do the check which avoids the need for this tricky hack altogether? You can put it in the table or metatable to make it easier to use. eg. It's syntax usage becomes _G:exists "this_should_be_nil" instead of _G.this_should_be_nilSwinge
The challenge for me was not solving the problem of needing a dynamic method (without a doubt, there are better ways in Lua than replicating method_missing), but simply seeing if it was linguistically possible to replicate it. With this last piece it seems like it is. (Albeit with the caveat that unless you also implement a 'responds_to' like function, it makes calls all to nil go silent)Berhley
@lhf This will make all nil values callable which is a quite elementary change of the Lua language. For example, (nil)() is suddenly perfectly valid code. It is also not what the method_missing method in Ruby does.Astraddle
D
2

You have identified the problem well: it is not, as far as I know, possible to solve that issue in pure Lua.

EDIT: I was wrong, you can by making nil callable. See other answers. It is still a bad idea IMO. The main use case for method_missing is proxy objects and you can solve that in another way. method_missing on Kernel (Ruby) / _G (Lua) is terrible :)

What you could do is only handle some methods, for instance if you know you expect methods that start by test_:

local function is_handled(method_name)
    return method_name:sub(1,5) == "test_"
end

function method_missing(selfs, func)

    local meta = getmetatable(selfs)
    local f
    if meta then
        f = meta.__index
    else
        meta = {}
        f = rawget
    end
    meta.__index = function(self, name)
        local v = f(self, name)
        if v then
            return v
        end

        if is_handled(name) then
            local metahack = {
                __call = function(self, ...)
                    return func(selfs, name, ...)
                end
            }
            return setmetatable({}, metahack)
        end
    end

    setmetatable(selfs, meta)
end

_G:method_missing(function(self, name, ...)
    if name=="test_print" then
        print("Oh my lord, it's method missing!", ...)
    end
end)

test_print("I like me some method_missing abuse!")

print(this_should_be_nil)

Now maybe the question should be: why do you want to replicate method_missing, and can you avoid it? Even in Ruby it is advised to avoid the use of method_missing and prefer dynamic method generation when possible.

Diplocardiac answered 4/11, 2013 at 22:28 Comment(0)
B
1

So with the tip from @lhf, I've managed a passable double (from what I can tell) of method_missing. In the end, I developed the following:

local field = '__method__missing'

function method_missing(selfs, func)

    local meta = getmetatable(selfs)
    local f
    if meta then
        f = meta.__index
    else
        meta = {}
        f = rawget
    end
    meta.__index = function(self, name)
        local v = f(self, name)
        if v then
            return v
        end

        rawget(self, name)[field] = function(...)
            return func(self, name, ...)
        end
    end

    setmetatable(selfs, meta)
end

debug.setmetatable(nil, { __call = function(self, ...) 
    if self[field] then
        return self[field](...)
    end
    return nil
end, __index = function(self, name) 
    if name~=field then error("attempt to index a nil value") end
    return getmetatable(self)[field]
end, __newindex = function(self, name, value)
    if name~=field then error("attempt to index a nil value") end
    getmetatable(self)[field] = value
end} )

_G:method_missing(function(self, name, ...)
    local args = {...}
    if name=="test_print" then
        print("Oh my lord, it's method missing!", ...)
        return
    elseif args[1] and string.find(name, args[1]) then --If the first argument is in the name called... 
        table.remove(args, 1)
        return unpack(args)
    end
end)

test_print("I like me some method_missing abuse!")
test_print("Do it again!")

print(test_print, "method_missing magic!")
print(this_should_be_nil == nil, this_should_be_nil() == nil)

print(conditional_runs("runs", "conditionally", "due to args"))
print(conditional_runs("While this does nothing!")) --Apparently this doesn't print 'nil'... why?

Output:

Oh my lord, it's method missing!        I like me some method_missing abuse!
Oh my lord, it's method missing!        Do it again!
nil     method_missing magic!
true    true
conditionally   due to args

This snippet lets you use method_missing pretty similarly to how you can in Ruby (with no response checking whatsoever, though). It's similar to my initial response, except it 'passes the buck' through nil's metatable, something I thought I couldn't do. (thanks for the tip!) But as @greatwolf says, there is probably no reason to ever use a construct like this in Lua; the same dynamism can probably be achieved through more clear metamethod manipulations.

Berhley answered 5/11, 2013 at 4:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.