Add where and first as methods to table, which accept predicates

This thread proposes changes relating primarily to syntactic sugar. Practically speaking, I’m not sure of how impactful this is. Probably something in the ballpark of Roblox adding table.find.

Right now, all methods for tables that come out of the box (minus foreach and its siblings) accept direct values. This is inclusive of Roblox’s own table.find. Over my time working with Luau and getting used to other languages in my spare time, I’ve found that having methods for tables that accept predicates is something really awesome when it comes to other languages. Having these sorts of things for arrays or other enumerable/iterable objects is overall really awesome and shortens code quite a bit. An immediate side effect of this implementation is that Luau is not going to support lambda expressions, and when it comes to methods accepting predicates, lambda expressions go together with predicates like cake and ice cream. So it may be a bit strange having to use anonymous functions in their place for some developers. No matter –

To work around this inconvenience, I implement my own extension module, which enables me to write code such as

local objects = table.where(workspace.Something:GetChildren(), function(obj)
    return obj:IsA("BasePart") and obj.Name == "Thingy"
end)

(which would return all instances in workspace.Something that cause that function to return true.)

A number of other similar predicate-accepting methods exist in my module, to name them:

  • table.contains – Returns whether or not the table has at least one element that satisfies a predicate.
  • table.where – Returns a new array containing all elements that satisfy a predicate.
  • table.first – Returns the first element that satisfies a predicate.
  • table.count – Returns the amount of elements that satisfy a predicate.
  • table.removeWhere – Removes all elements from the given array that satisfy a predicate.
  • table.transform – Modifies every value in the array with the given modification function, which returns a replacement value, or nil to remove that item.

All of these function on arrays only, and do not support iterating over non-linear integer keys.

In my opinion, the most important of these are where and first. Every other method in that list could be created from these, which makes them the stars of this feature request.

10 Likes

I don’t see the appeal for these compared to iterating through tables and running the comparison inline. At a glance, it might even be more boilerplate to use these: for i, v in ipairs(foo) do vs table.transform(foo, function(item) end).

I can see the appeal for a filtering function (removeWhere in your case) since that’s annoying to implement manually, but otherwise I’m not sure what the benefit is except replacing yesterday’s boilerplate with today’s fresh new boilerplate.

1 Like

This will probably encourage slower code

as in this example, a function object is created each time this code is ran (also it should use ==). If the code runs multiple times then it should create the function object outside and pass it in every time (assuming it doesn’t need to access external local variables). Passing functions for conditions works fine in other languages because of optimization, but Luau can’t do so many optimizations because of various language features (namely getfenv and setfenv).

local function filter(obj)
	return obj:IsA"BasePart" and obj.Name == "Thingy"
end
-- ...
local objects = table.where(workspace.Something:GetChildren(),filter)

The example would probably be better to use table.removeWhere which you suggested, to avoid creating unnecessary tables.

local function filter(obj)
	return not obj:IsA"BasePart" or obj.Name ~= "Thingy"
end
-- ...
local objects = workspace.Something:GetChildren()
table.removeWhere(objects,filter)

There are some uses for passing extra values to the functions

local Workspace = game:GetService"Workspace"
local Map = Workspace:FindFirstChild"Map"
local MapDescendants = Map:GetDescendants()
local MapBaseParts = table.where(MapDescendants,game.IsA,"BasePart")
-- so that calls to IsA would become IsA(object,"BasePart")
-- which would check object is a BasePart

would these be supported?

Since this objects will be iterating over arrays, is their iteration order defined? Many of the functions could be implemented using reverse iteration order, or even random iteration order.

I’m not seeing this. Also, odd of me to make a typo with the = operator. Fail. I’ll edit OP.

Using this test code:

local table = require(game:GetService("ReplicatedStorage").EtiLibs.Extension.Table)
local tbl = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
local tbl2 = table.where(tbl, function (element)
	return element % 2 == 0
end)

And where table.where is defined as (note the print statement)

Table.where = function (tbl, predicate)
	local result = table.create(#tbl)
	for i = 1, #tbl do
		local object = tbl[i]
		print(predicate)
		if (predicate(object)) then
			table.insert(result, object)
		end
	end
	return result
end

The output logs the same function memory address 12 times in a row.
image

As far as my personal module goes? No, use cases too narrow for my design patterns. As far as the iteration order goes, I’m just doing a straight 1 - length iteration, nothing too out of the ordinary (except for removeWhere which operates in reverse so that table.remove does not interfere with indices)

My module offers a few methods for table reversing and whatnot, I tried to optimize it but I’m not exactly the best at doing so. Extensions.Table.lua · GitHub (Edit: This seems to be the wrong version with a few doc problems and older code e.g. transform returns a new table. Oh well. You can fill in the blanks of what the module’s general purpose is.)

Most of these are indeed what Dekkonot said - boilerplate, and if I had to add a word, niche. Most of this was done for an alarmingly common reason from me - I found it in other languages, wanted it in Luau, made it myself.

What I meant by create a function object every time is that, every time the whole call to the function is ran, it creates a function object.

local tbl = {{1,2,3},{4,5,6},{7,8,9}}
for _,v in ipairs(tbl) do
	table.removeWhere(tbl,function(x)return x%2 == 0 end)
	-- a function object is created every iteration
end

While putting the function outside will only create it once:

local function IsEven(x)
	return x%2 == 0
end
local tbl = {{1,2,3},{4,5,6},{7,8,9}}
for _,v in ipairs(tbl) do
	table.removeWhere(tbl,IsEven)
	-- the same function object gets passed every time
end
1 Like