Ruby Constants Have Non-Deterministic Order
I was pairing on some Foobara code with Hector Miramontes who I know from the SF Ruby Meetup when we ran into a difficult-to-explain test failure on his machine.
Now, Hector uses a Mac and I use Linux.
Here's an example of the error we were seeing consistently on his Mac:
Failures:
1) :float caster names gives the names of the casters including the dynamically generated class matcher
Failure/Error:
expect(type.value_caster.processor_names).to eq(["Casters::Integer", "Casters::String"])
expected: ["Casters::Integer", "Casters::String"]
got: ["Casters::String", "Casters::Integer"]
So strange! Those elements were reversed on our systems.
At first, I thought maybe it was caused by files being loaded in different orders in the two different filesystems/OSes. But I was pretty sure I'd already addressed that in the past.
So what gives??
We dug in and discovered that Ruby's constants are not returned in deterministic order!
Check out this script we wrote and its output:
ruby_version = [RUBY_ENGINE_VERSION, RUBY_VERSION].uniq.join("/") ruby_version = "#{ruby_version}.p#{RUBY_PATCHLEVEL}" ruby_version = [RUBY_ENGINE, ruby_version, RUBY_PLATFORM, RUBY_RELEASE_DATE].join(" ") puts ruby_version puts (2..).each do |constant_count| m = Module.new constant_count.times { m.const_set("C#{it}", it) } if m.constants.sort_by { it[1..].to_i } != m.constants puts m.constants puts puts "Constants became unordered once there were #{constant_count} constants" break end end
This just dynamically creates constants in a module until the constants no longer come back in the order that they were created.
On my Linux machine, this outputs:
ruby 3.4.7.p58 x86_64-linux 2025-10-08
C31
C0
C1
C2
C3
C4
C5
C6
C7
C8
C9
C10
C11
C12
C13
C14
C15
C16
C17
C18
C19
C20
C21
C22
C23
C24
C25
C26
C27
C28
C29
C30
Constants became unordered once there were 32 constants
But on Hector's Mac, this outputs:
ruby 3.4.7.p58 arm64-darwin24 2025-10-08
C3
C0
C1
C2
Constants became unordered once there were 4 constants
Well that caught me off guard!
The solution was pretty straightforward: explicitly order Casters.constants by name to make it deterministic.
I had assumed that constants would be in a deterministic order since Ruby has had ordered hash tables since version 1.9, and I believe that, internally, Ruby stores constants in a hash table.
I suspect Ruby uses a different type of hash table for Hash than it uses for certain internal hash tables
such as constants in modules.
Who knew! (Well, probably plenty knew, but not me, until now!)