Params objects and Random should compare by reference with comparison operators

Currently the Params objects (RaycastParams, CatalogSearchParams, and probably OverlapParams) and Random overload the comparison operators (__eq) and define them to compare by value. The Params objects and Random appear to be the only mutable data types which define comparison to be by value. Even other mutable Roblox types compare by reference, e.g. Instance, RBXScriptSignal, and RBXScriptConnection compare by reference.

Examples of comparison with Params objects and Random vs other types
local r1 = RaycastParams.new()
local r2 = RaycastParams.new()
local r3 = r1
-- r1 and r2 are compared by value, and they are considered equal
-- as all of their properties are equivalent
print(r1==r2) --> true
print(r1==r3) --> true
r1.IgnoreWater = true
print(r1==r2) --> false
print(r1==r3) --> true
local r1 = Random.new(1)
local r2 = r1:Clone()
local r3 = r1
-- r1 and r2 are compared by value, and they are considered equal
-- as r2 has the same state as r1
print(r1==r2) --> true
print(r1==r3) --> true
r1:NextNumber()
print(r1==r2) --> false
print(r1==r3) --> true

Tables without the __eq metamethod are compared by reference, which means that the contents of the tables don’t affect equality comparisons.

local t1 = {1}
local t2 = {1}
local t3 = t1
-- if t1 and t2 were compared by value, they would be considered equal
-- as both have the same indices with equal values
print(t1==t2) --> false
print(t1==t3) --> true
t1[1] = 2
print(t1==t2) --> false
print(t1==t3) --> true

And with other mutable types

local i1 = Instance.new"IntValue"
local i2 = Instance.new"IntValue"
local i3 = i1
i1.Value = 1
i2.Value = 1
-- if i1 and i2 were compared by value, they would be considered equal
-- as all of their properties are equal
print(i1==i2) --> false
print(i1==i3) --> true
i1.Value = 2
print(i1==i2) --> false
print(i1==i3) --> true
local e1 = Instance.new"BindableEvent".Event
local e2 = Instance.new"BindableEvent".Event
local e3 = e1
-- if e1 and e2 were compared by value, they would be considered equal
-- as they both have no connections
-- and are both events for BindableEvent instances
print(e1==e2) --> false
print(e1==e3) --> true
local c1 = e1:Connect(print)
local c2 = e1:Connect(print)
local c3 = c1
print(e1==e2) --> false
print(e1==e3) --> true
-- if c1 and c2 were compared by value, they would be considered equal
-- as they both are connected with the same functions to the same event
print(c1==c2) --> false
print(c1==c3) --> true
c1:Disconnect()
print(c1==c2) --> false
print(c1==c3) --> true
local function f()
	return function()return x end
end
local f1 = f()
local f2 = f()
local f3 = a
local t1 = {x=1}
local t2 = {x=2}
setfenv(f1,t1)
setfenv(f2,t1)
-- if f1 and f2 were compared by value, they would be considered equal
-- as they came from exactly the same place (same function body and debug info)
-- and they have the same environments
print(f1==f2) --> false
print(f1==f3) --> true
setfenv(f1,t2)
print(f1==f2) --> false
print(f1==f3) --> true
local function f(e)
	setfenv(0,e)
	while true do
		setfenv(0,coroutine.yield())
	end
end
local c1 = coroutine.create(f)
local c2 = coroutine.create(f)
local c3 = c1
local t1 = {x=1}
local t2 = {x=2}
coroutine.resume(c1,t1)
coroutine.resume(c2,t1)
-- if c1 and c2 were compared by value, they would be considered equal
-- as they both have the same environment
-- and they have the same functions
-- and they both yielded in the same position
-- and the values which they contain (their stack) are equivalent
print(c1==c2) --> false
print(c1==c3) --> true
coroutine.resume(c1,t2)
print(c1==c2) --> false
print(c1==c3) --> true

Changing Params objects and Random to compare by reference would make them more consistent with other mutable types, namely tables. Comparing mutable objects by reference is usually the expected behavior in comparisons, and is the current behavior of all other mutable objects. If two variables that contain references to mutable objects compare equal, it makes sense that updating the contents of the object referenced by one variable is seen when reading the contents of the object referenced by the other variable. The current behavior breaks this intuition and is quite unexpected and inconsistent.

Example use case: An array of RaycastParams is used for multiple raycasts with the same position and direction (perhaps to see what is hit only considering certain parts, for multiple arrays of parts). To find if a specific RaycastParams is in that array or to get the index of a specific RaycastParams, something like table.find currently can’t be used as it uses __eq (which currently compares by value), so it may get the index of a different RaycastParams object. If comparisons were by reference, it would work as expected and return the index to the RaycastParams object even if another RaycastParams object which is equivalent when compared by value comes before it in the array.

Comparing Random objects by value seems particularly weird, could the state at some point repeat after some very large number of calls to NextNumber and cause it to compare equal to some unchanged Random object which the mutated Random object was cloned from?

Example of large number of calls to random
local r1 = Random.new(123456789)
local r2 = r1:Clone()
local c1 = 1
local c2 = 0
local c3 = 0
local c4 = 0
local c5 = 0
r2:NextNumber()
while r1 ~= r2 do
	-- is this an infinite loop?
	-- or does it eventually terminate?
	c1 += 1
	if c1 == 1E9 then
		c2 += 1
		if c2 == 1E9 then
			c3 += 1
			if c3 == 1E9 then
				c4 += 1
				if c4 == 1E9 then
					c5 += 1
					c4 = 0
				end
				c3 = 0
			end
			c2 = 0
		end
		c1 = 0
	end
	r2:NextNumber()
end
print(string.format("%i%09i%09i%09i%09i",c5,c4,c3,c2,c1))
2 Likes