Search code examples
lualua-api

Equality operator on mixed types in Lua


In chapter 13.2 of Programming in Lua it's stated that

Unlike arithmetic metamethods, relational metamethods do not support mixed types.

and at the same time

Lua calls the equality metamethod only when the two objects being compared share this metamethod

So I'm implementing my library in C and want to be able to support behavior like

a = A()
b = B()
a == b

by providing

static const struct luaL_Reg mylib_A[] =
{
  { "__eq", my_equal }
  , <more stuff>
  , { NULL, NULL }
};

and

static const struct luaL_Reg mylib_B[] =
{
  { "__eq", my_equal }
  , <more stuff>
  , { NULL, NULL }
};

Which doesn't seem to work, is there a workaround for this?
Note: my_equal is able to handle both userdata of type A and type B in any of it's arguments

UPDATE: Metatables registration:

luaL_newmetatable(lua, "B");
lua_pushvalue(lua, -1);
lua_setfield(lua, -2, "__index");
luaL_register(lua, NULL, mylib_B);

luaL_newmetatable(lua, "A");
lua_pushvalue(lua, -1);
lua_setfield(lua, -2, "__index");
luaL_register(lua, NULL, mylib_A);

luaL_register(lua, "mylib", mylib); -- where mylib is a bunch of static functions

Application code:

require 'mylib'
a = mylib.new_A()
b = mylib.new_B()
a == b -- __eq is not called

Solution

  • EDIT: Also see whoever's answer which has a particular caveat with regards to implementing __eq in the C API.


    The __eq metamethod belongs in your metatable, not in the __index table.

    In lua:

    function my_equal(x,y)
        return x.value == y.value
    end
    
    
    
    A = {} -- luaL_newmetatable(lua, "A");
    A.__eq = my_equal
    
    function new_A(value)
        local a = { value = value }
        return setmetatable(a, A)
    end
    
    
    B = {} -- luaL_newmetatable(lua, "B");
    B.__eq = my_equal
    
    function new_B(value)
        local b = { value = value }
        return setmetatable(b, B)
    end
    
    
    a = new_A()
    b = new_B()
    print(a == b) -- __eq is called, result is true
    
    a.value = 5
    print(a == b) -- __eq is called, result is false
    

    What you have done is this:

    myLib_A = {}
    myLib_A.__eq = my_equal
    
    A = {} -- luaL_newmetatable(lua, "A");
    A.__index = myLib_A
    

    Note that __eq is not in A's metatable, it's on a totally separate table that you just be happen to be using in a different, unrelated metamethod (__index). Lua is not going to look there when trying to resolve the equality operator for a.

    The Lua manual explains this in detail:

    "eq": the == operation. The function getcomphandler defines how Lua chooses a metamethod for comparison operators. A metamethod only is selected when both objects being compared have the same type and the same metamethod for the selected operation.

     function getcomphandler (op1, op2, event)
       if type(op1) ~= type(op2) then return nil end
       local mm1 = metatable(op1)[event]
       local mm2 = metatable(op2)[event]
       if mm1 == mm2 then return mm1 else return nil end
     end
    

    The "eq" event is defined as follows:

     function eq_event (op1, op2)
       if type(op1) ~= type(op2) then  -- different types?
         return false   -- different objects
       end
       if op1 == op2 then   -- primitive equal?
         return true   -- objects are equal
       end
       -- try metamethod
       local h = getcomphandler(op1, op2, "__eq")
       if h then
         return (h(op1, op2))
       else
         return false
       end
     end
    

    So when Lua encounters result = a == b, it's going to do the following (this is done in C, Lua used as pseudocode here):

    -- Are the operands are the same type? In our case they are both tables:
    if type(a) ~= type(b) then
     return false
    end
    
    -- Are the operands the same object? This comparison is done in C code, so
    -- it's not going to reinvoke the equality operator.
    if a ~= b then
     return false
    end
    
    -- Do the operands have the same `__eq` metamethod?
    local mm1 = getmetatable(a).__eq
    local mm2 = getmetatable(b).__eq
    if mm1 ~= mm2 then
     return false
    end
    
    -- Call the `__eq` metamethod for the left operand (same as the right, doesn't really matter)
    return mm1(a,b)
    

    You can see there's no path here that results in resolve a.__eq, which would resolve to myLib_A through your __index metamethod.