Search code examples
luaoperatorslua-tableevaluation

Using metatables for adding two different types


I have fractions table which stores fractions and fractions get associated with its metatable. fraction.add is metaoperation (__add). Here is the problem. I want to evaluate x + y.

If x and y are both fractions then there is no issue in evaluating it. It gets evaluated withfarction.add.

If both x and y are numbers, which are not fractions, then it is simple addition of numbers, which has nothing to do with fractions.

If x is fraction and y is non-fraction number (or vice-versa), then I want to convert fraction x into number and add it to y. I of course can easily convert fraction into number, but the problem is with __add metatable of fraction which is associated with fraction.add. It works only when both x and y are fractions. It throws error as y is not fraction. So I am looking for the logic that will handle the case when one is fraction and other is non-fraction number. It probably is simple if else in metatable, but I am not able to do it. Any ideas would really be helpful.


Solution

  • I've implemented a full-fledged such fraction "class" with support for this. It's as simple as checking the types of the arguments a, b of the __add metamethod and converting them to fractions if needed.

    Or, for a more minimal example, which skimps on a couple points:

    • Fractions aren't shortened. Float / integer imprecisions / overflows will ultimately result in inaccurate fractions. Ideally you should use some kind of big integers for numerator and denominator, and shorten after every operation.
    • For simplicity, only integers will be converted to fraction; floats will raise an error, since converting floats to fractions is more tricky.
    • It's missing plenty of operations.
    local fraction_mt = {}
    
    local function new_fraction(numerator, denominator)
        assert(numerator % 1 == 0)
        assert(denominator % 1 == 0)
        return setmetatable({numerator = numerator, denominator = denominator}, fraction_mt)
    end
    
    local function add_fractions(a, b)
        -- Note: No care is taken to shorten the resulting fraction here.
        return new_fraction(a.numerator * b.denominator + b.numerator * a.denominator,
            a.denominator * b.denominator)
    end
    
    function fraction_mt.__add(a, b)
        if type(a) == "number" then
            a = new_fraction(a, 1)
        elseif type(b) == "number" then
            b = new_fraction(b, 1)
        end
        return add_fractions(a, b)    
    end
    
    function fraction_mt:__tostring()
        return ("%d/%d"):format(self.numerator, self.denominator)
    end
    
    print(new_fraction(1, 2) + new_fraction(1, 2)) -- 4/4
    print(new_fraction(1, 2) + 1) -- 3/2
    print(1 + new_fraction(1, 2)) -- 3/2
    

    Note that Lua will call __add even if only one of the operands is not a number (see the reference manual):

    If any operand for an addition is not a number, Lua will try to call a metamethod. It starts by checking the first operand (even if it is a number); if that operand does not define a metamethod for __add, then Lua will check the second operand. If Lua can find a metamethod, it calls the metamethod with the two operands as arguments, and the result of the call (adjusted to one value) is the result of the operation. Otherwise, if no metamethod is found, Lua raises an error.


    After clarification: To convert a fraction to a number before adding, producing a number for fraction + number or number + fraction, simply use:

    function fraction_mt:tonumber()
        return self.numerator / self.denominator
    end
    
    function fraction_mt.__add(a, b)
        if type(a) == "number" then
            return a + b:tonumber()
        end if type(b) == "number" then
            return a:tonumber() + b
        end
        return add_fractions(a, b)
    end