Lua Collections

Dec. 08 2020

I've been using Laravel for a couple years now. We use it at the office to build our very own open source CMS. I enjoy using it, not only because it's ease of use, extendability, tons of built-in, useful libraries, and vast ecosystem, but also it's approach to writing clean, readable code.

One particular feature is it's Collection class, which is a wrapper around the standard PHP Array (i.e. an ordered map). Collections extend the functionality of the Array you provide it through a fluent, chainable interface.

For example, say we only want the lastName values from the $names Array:

$names = [
    [ firstName = 'John', lastName = 'Smith' ],
    [ firstName = 'Jane', lastName = 'Doe' ],
];

// ----
// Standard
$lastNames = [];

foreach ($names as $key => $value) {
  $lastNames[] += $value['lastName'];
}

// ----
// Using Collections
$lastNames = collect($names)->map(function($item) {
    return $item['lastName'];
});

Notice how our code immediately becomes more readable. We utilize the map helper method to update the $names Array with only what we want. It returns a new Array called $lastNames, which just holds what we need. Furthermore, you can chain it with another method (e.g. filter, to filter out names based on custom criteria).

Onward

As a challenge, I wanted to see if I could take the same idea and apply it to tables in Lua. I'll be referencing the list of Available Methods for Collections and implementing them based on usefulness.

The full, up-to-date code is available here.

Setup

First we'll need a way to hold our table data. We'll setup a simple class to make things neater if we wanted to export our code into a separate file to be included elsewhere.

Item = {}
Item.__index = Item

-- initializer
function Item:create(items)
    assert(type(items) == 'table', 'Item expects a table.')

    self.__index = self

    return setmetatable({ items = items }, Item)
end

-- pretty print
function Item:__tostring()
    local out = {}

        for k, v in pairs(self.items) do
                table.insert(out, string.format('%s=%s', k, v))
        end

        return '{ ' .. table.concat(out, ', ') .. ' }'
end

With this approach we defer function calls to Item, and also allows us to use meta methods like __tostring to print our output in a readable format.

Let's also introduce a tiny helper function to serve up an instance of our Item class.

function Items(...)
    return Item:create(...)
end

Available Functions

Map

-- iterates through each item, sending it's
-- key and value through the given callback
function Item:map(cb)
    for k, v in pairs(self.items) do
        self.items[k] = cb(v, k)
    end

    return self
end
Usage
-- input
local items = { a = 1, b = 2, c = 3, d = 4, e = 5 }

-- double each value
local newItems = Items(items):map(function(v, k)
    return v * 2
end)

-- output
print(newItems) -- { a=2, b=4, c=6, d=8, e=10 }

Filter

-- iterates though each item, filters out
-- non-truthy values returned by given callback
function Item:filter(cb)
    for k, v in pairs(self.items) do
    if not cb(v, k) then
        return self.items[k] = nil
    end
  end
end
Usage
-- input
local items = { a = 1, b = 2, c = 3, d = 4, e = 5 }

-- double each value
items = Items(items):filter(function(v, k)
    return v > 3
end)

-- output
print(items) -- { d=4, e=5 }

Reject

-- iterates though each item, filters out
-- truthy values returned by given callback
function Item:reject(cb)
    for k, v in pairs(self.items) do
    if cb(v, k) then
        return self.items[k] = nil
    end
  end
end
Usage
-- input
local items = { a = 1, b = 2, c = 3, d = 4, e = 5 }

-- double each value
items = Items(items):reject(function(v, k)
    return v > 3
end)

-- output
print(items) -- { a=1, b=2, c=3 }

Merge

-- merge new tbl into original table,
-- overriding existing values
function Item:merge(tbl)
  if type(tbl) == 'table' then
    for k, v in pairs(tbl) do
      self.items[k] = v
    end
  end

    return self
end
Usage
-- input
local itemsA = { a = 1, b = 2, c = 3, d = 4, e = 5 }
local itemsB = { a = 6, f = 8 }

-- double each value
itemsA = Items(itemsA):merge(itemsB)

-- output
print(itemsA) -- { a=6, b=2, c=3, d=4, e=5, f=8  }

Let's break for now...

This article is getting a bit lengthy, so I'll end it here. Stay tuned for further parts & gist for updates. Thanks for reading!