Class: Zeitwerk::Cref::Map
- Inherits:
-
Object
- Object
- Zeitwerk::Cref::Map
- Defined in:
- lib/zeitwerk/cref/map.rb
Overview
This class emulates a hash table whose keys are of type Zeitwerk::Cref.
It is a synchronized 2-level hash. The keys of the top one, stored in ‘@map`, are class and module objects, but their hash code is forced to be their object IDs (see why below). Then, each one of them stores a hash table keyed on constant names as symbols. We finally store the values in those.
For example, if we store values 0, 1, and 2 for the crefs that would correspond to ‘M::X`, `M::Y`, and `N::Z`, the map will look like this:
{ M => { X: 0, :Y => 1 }, N => { Z: 2 } }
This structure is internal, so only the needed interface is implemented.
Why not use tables that map pairs [Module, Symbol] to their values? Because class and module objects are not guaranteed to be hashable, the ‘hash` method may have been overridden:
https://github.com/fxn/zeitwerk/issues/188
We can also use a 1-level hash whose keys are the corresponding class and module names. In the example above it would be:
{ "M::X" => 0, "M::Y" => 1, "N::Z" => 2 }
The gem used this approach for several years.
Another option would be to make crefs hashable. I tried with hash code
real_mod_hash(mod) ^ cname.hash
and the matching eql?, but that was about 1.8x slower.
Finally, I came with this solution which is 1.6x faster than the previous one based on class and module names, even being synchronized. Also, client code feels natural, since crefs are central objects in Zeitwerk’s implementation.
Instance Method Summary collapse
- #[](cref) ⇒ Object
- #[]=(cref, value) ⇒ Object
- #clear ⇒ Object
- #delete(cref) ⇒ Object
- #delete_by_value(value) ⇒ Object
-
#delete_mod_cname(mod, cname) ⇒ Object
Ad-hoc for loader_for, called from const_added.
-
#each_key ⇒ Object
Order of yielded crefs is undefined.
- #empty? ⇒ Boolean
- #get_or_set(cref, &block) ⇒ Object
-
#initialize ⇒ Map
constructor
:nodoc: all.
Constructor Details
#initialize ⇒ Map
:nodoc: all
40 41 42 43 44 |
# File 'lib/zeitwerk/cref/map.rb', line 40 def initialize @map = {} @map.compare_by_identity @mutex = Mutex.new end |
Instance Method Details
#[](cref) ⇒ Object
55 56 57 58 59 |
# File 'lib/zeitwerk/cref/map.rb', line 55 def [](cref) @mutex.synchronize do @map[cref.mod]&.[](cref.cname) end end |
#[]=(cref, value) ⇒ Object
47 48 49 50 51 52 |
# File 'lib/zeitwerk/cref/map.rb', line 47 def []=(cref, value) @mutex.synchronize do cnames = (@map[cref.mod] ||= {}) cnames[cref.cname] = value end end |
#clear ⇒ Object
112 113 114 115 116 |
# File 'lib/zeitwerk/cref/map.rb', line 112 def clear @mutex.synchronize do @map.clear end end |
#delete(cref) ⇒ Object
70 71 72 |
# File 'lib/zeitwerk/cref/map.rb', line 70 def delete(cref) delete_mod_cname(cref.mod, cref.cname) end |
#delete_by_value(value) ⇒ Object
89 90 91 92 93 94 95 96 |
# File 'lib/zeitwerk/cref/map.rb', line 89 def delete_by_value(value) @mutex.synchronize do @map.delete_if do |mod, cnames| cnames.delete_if { _2 == value } cnames.empty? end end end |
#delete_mod_cname(mod, cname) ⇒ Object
Ad-hoc for loader_for, called from const_added. That is a hot path, I prefer to not create a cref in every call, since that is global.
78 79 80 81 82 83 84 85 86 |
# File 'lib/zeitwerk/cref/map.rb', line 78 def delete_mod_cname(mod, cname) @mutex.synchronize do if cnames = @map[mod] value = cnames.delete(cname) @map.delete(mod) if cnames.empty? value end end end |
#each_key ⇒ Object
Order of yielded crefs is undefined.
101 102 103 104 105 106 107 108 109 |
# File 'lib/zeitwerk/cref/map.rb', line 101 def each_key @mutex.synchronize do @map.each do |mod, cnames| cnames.each_key do |cname| yield Zeitwerk::Cref.new(mod, cname) end end end end |
#empty? ⇒ Boolean
119 120 121 122 123 |
# File 'lib/zeitwerk/cref/map.rb', line 119 def empty? # for tests @mutex.synchronize do @map.empty? end end |
#get_or_set(cref, &block) ⇒ Object
62 63 64 65 66 67 |
# File 'lib/zeitwerk/cref/map.rb', line 62 def get_or_set(cref, &block) @mutex.synchronize do cnames = (@map[cref.mod] ||= {}) cnames.fetch(cref.cname) { cnames[cref.cname] = block.call } end end |