Skip to content

Commit 41b8e44

Browse files
committed
Support backwards compatibility for Set subclasses
For subclasses from Set, require `set/subclass_compatible`, and extend the subclass and include a module in it that makes it more backwards compatible with the pure Ruby Set implementation used before Ruby 4. The module included in the subclass contains a near-copy of the previous Set implementation, with the following changes: * Accesses to `@hash` are generally replaced with `super` calls. In some cases, they are replaced with a call to another instance method. * Some methods that only accessed `@hash` and nothing else are not defined, so they inherit behavior from core Set. * The previous `Set#divide` implementation is not used, to avoid depending on tsort. This fixes the following two issues: * [Bug #21375] Set[] does not call #initialize * [Bug #21396] Set#initialize should call Set#add on items passed in It should also fix the vast majority of backwards compatibility issues in other cases where code subclassed Set and depended on implementation details (such as which methods call which other methods). This does not affect Set internals, so Set itself remains fast. For users who want to subclass Set but do not need to worry about backwards compatibility, they can subclass from Set::CoreSet, a Set subclass that does not have the backward compatibility layer included.
1 parent a24922a commit 41b8e44

File tree

3 files changed

+421
-16
lines changed

3 files changed

+421
-16
lines changed

lib/set/subclass_compatible.rb

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
# frozen_string_literal: true
2+
3+
# :markup: markdown
4+
#
5+
# set/subclass_compatible.rb - Provides compatibility for set subclasses
6+
#
7+
# Copyright (c) 2002-2024 Akinori MUSHA <knu@iDaemons.org>
8+
#
9+
# Documentation by Akinori MUSHA and Gavin Sinclair.
10+
#
11+
# All rights reserved. You can redistribute and/or modify it under the same
12+
# terms as Ruby.
13+
14+
15+
class Set
16+
# This module is automatically included in subclasses of Set, to
17+
# make them backwards compatible with the pure-Ruby set implementation
18+
# used before Ruby 4. Users who want to use Set subclasses without
19+
# this compatibility layer should subclass from Set::CoreSet.
20+
#
21+
# Note that Set subclasses that access `@hash` are not compatible even
22+
# with this support. Such subclasses must be updated to support Ruby 4.
23+
module SubclassCompatible
24+
module ClassMethods
25+
def [](*ary)
26+
new(ary)
27+
end
28+
end
29+
30+
# Creates a new set containing the elements of the given enumerable
31+
# object.
32+
#
33+
# If a block is given, the elements of enum are preprocessed by the
34+
# given block.
35+
#
36+
# Set.new([1, 2]) #=> #<Set: {1, 2}>
37+
# Set.new([1, 2, 1]) #=> #<Set: {1, 2}>
38+
# Set.new([1, 'c', :s]) #=> #<Set: {1, "c", :s}>
39+
# Set.new(1..5) #=> #<Set: {1, 2, 3, 4, 5}>
40+
# Set.new([1, 2, 3]) { |x| x * x } #=> #<Set: {1, 4, 9}>
41+
def initialize(enum = nil, &block) # :yields: o
42+
enum.nil? and return
43+
44+
if block
45+
do_with_enum(enum) { |o| add(block[o]) }
46+
else
47+
merge(enum)
48+
end
49+
end
50+
51+
def do_with_enum(enum, &block) # :nodoc:
52+
if enum.respond_to?(:each_entry)
53+
enum.each_entry(&block) if block
54+
elsif enum.respond_to?(:each)
55+
enum.each(&block) if block
56+
else
57+
raise ArgumentError, "value must be enumerable"
58+
end
59+
end
60+
private :do_with_enum
61+
62+
def replace(enum)
63+
if enum.instance_of?(self.class)
64+
super
65+
else
66+
do_with_enum(enum) # make sure enum is enumerable before calling clear
67+
clear
68+
merge(enum)
69+
end
70+
end
71+
72+
def to_set(*args, &block)
73+
klass = if args.empty?
74+
Set
75+
else
76+
warn "passing arguments to Enumerable#to_set is deprecated", uplevel: 1
77+
args.shift
78+
end
79+
return self if instance_of?(Set) && klass == Set && block.nil? && args.empty?
80+
klass.new(self, *args, &block)
81+
end
82+
83+
def flatten_merge(set, seen = {}) # :nodoc:
84+
set.each { |e|
85+
if e.is_a?(Set)
86+
case seen[e_id = e.object_id]
87+
when true
88+
raise ArgumentError, "tried to flatten recursive Set"
89+
when false
90+
next
91+
end
92+
93+
seen[e_id] = true
94+
flatten_merge(e, seen)
95+
seen[e_id] = false
96+
else
97+
add(e)
98+
end
99+
}
100+
101+
self
102+
end
103+
protected :flatten_merge
104+
105+
def flatten
106+
self.class.new.flatten_merge(self)
107+
end
108+
109+
def flatten!
110+
replace(flatten()) if any?(Set)
111+
end
112+
113+
def superset?(set)
114+
case
115+
when set.instance_of?(self.class)
116+
super
117+
when set.is_a?(Set)
118+
size >= set.size && set.all?(self)
119+
else
120+
raise ArgumentError, "value must be a set"
121+
end
122+
end
123+
alias >= superset?
124+
125+
def proper_superset?(set)
126+
case
127+
when set.instance_of?(self.class)
128+
super
129+
when set.is_a?(Set)
130+
size > set.size && set.all?(self)
131+
else
132+
raise ArgumentError, "value must be a set"
133+
end
134+
end
135+
alias > proper_superset?
136+
137+
def subset?(set)
138+
case
139+
when set.instance_of?(self.class)
140+
super
141+
when set.is_a?(Set)
142+
size <= set.size && all?(set)
143+
else
144+
raise ArgumentError, "value must be a set"
145+
end
146+
end
147+
alias <= subset?
148+
149+
def proper_subset?(set)
150+
case
151+
when set.instance_of?(self.class)
152+
super
153+
when set.is_a?(Set)
154+
size < set.size && all?(set)
155+
else
156+
raise ArgumentError, "value must be a set"
157+
end
158+
end
159+
alias < proper_subset?
160+
161+
def <=>(set)
162+
return unless set.is_a?(Set)
163+
164+
case size <=> set.size
165+
when -1 then -1 if proper_subset?(set)
166+
when +1 then +1 if proper_superset?(set)
167+
else 0 if self.==(set)
168+
end
169+
end
170+
171+
def intersect?(set)
172+
case set
173+
when Set
174+
if size < set.size
175+
any?(set)
176+
else
177+
set.any?(self)
178+
end
179+
when Enumerable
180+
set.any?(self)
181+
else
182+
raise ArgumentError, "value must be enumerable"
183+
end
184+
end
185+
186+
def disjoint?(set)
187+
!intersect?(set)
188+
end
189+
190+
def add?(o)
191+
add(o) unless include?(o)
192+
end
193+
194+
def delete?(o)
195+
delete(o) if include?(o)
196+
end
197+
198+
def delete_if(&block)
199+
block_given? or return enum_for(__method__) { size }
200+
select(&block).each { |o| delete(o) }
201+
self
202+
end
203+
204+
def keep_if(&block)
205+
block_given? or return enum_for(__method__) { size }
206+
reject(&block).each { |o| delete(o) }
207+
self
208+
end
209+
210+
def collect!
211+
block_given? or return enum_for(__method__) { size }
212+
set = self.class.new
213+
each { |o| set << yield(o) }
214+
replace(set)
215+
end
216+
alias map! collect!
217+
218+
def reject!(&block)
219+
block_given? or return enum_for(__method__) { size }
220+
n = size
221+
delete_if(&block)
222+
self if size != n
223+
end
224+
225+
def select!(&block)
226+
block_given? or return enum_for(__method__) { size }
227+
n = size
228+
keep_if(&block)
229+
self if size != n
230+
end
231+
232+
alias filter! select!
233+
234+
def merge(*enums, **nil)
235+
enums.each do |enum|
236+
if enum.instance_of?(self.class)
237+
super(enum)
238+
else
239+
do_with_enum(enum) { |o| add(o) }
240+
end
241+
end
242+
243+
self
244+
end
245+
246+
def subtract(enum)
247+
do_with_enum(enum) { |o| delete(o) }
248+
self
249+
end
250+
251+
def |(enum)
252+
dup.merge(enum)
253+
end
254+
alias + |
255+
alias union |
256+
257+
def -(enum)
258+
dup.subtract(enum)
259+
end
260+
alias difference -
261+
262+
def &(enum)
263+
n = self.class.new
264+
if enum.is_a?(Set)
265+
if enum.size > size
266+
each { |o| n.add(o) if enum.include?(o) }
267+
else
268+
enum.each { |o| n.add(o) if include?(o) }
269+
end
270+
else
271+
do_with_enum(enum) { |o| n.add(o) if include?(o) }
272+
end
273+
n
274+
end
275+
alias intersection &
276+
277+
def ^(enum)
278+
n = self.class.new(enum)
279+
each { |o| n.add(o) unless n.delete?(o) }
280+
n
281+
end
282+
283+
def ==(other)
284+
if self.equal?(other)
285+
true
286+
elsif other.instance_of?(self.class)
287+
super
288+
elsif other.is_a?(Set) && self.size == other.size
289+
other.all? { |o| include?(o) }
290+
else
291+
false
292+
end
293+
end
294+
295+
def eql?(o) # :nodoc:
296+
return false unless o.is_a?(Set)
297+
super
298+
end
299+
300+
def classify
301+
block_given? or return enum_for(__method__) { size }
302+
303+
h = {}
304+
305+
each { |i|
306+
(h[yield(i)] ||= self.class.new).add(i)
307+
}
308+
309+
h
310+
end
311+
312+
def join(separator=nil)
313+
to_a.join(separator)
314+
end
315+
316+
InspectKey = :__inspect_key__ # :nodoc:
317+
318+
# Returns a string containing a human-readable representation of the
319+
# set ("#<Set: {element1, element2, ...}>").
320+
def inspect
321+
ids = (Thread.current[InspectKey] ||= [])
322+
323+
if ids.include?(object_id)
324+
return sprintf('#<%s: {...}>', self.class.name)
325+
end
326+
327+
ids << object_id
328+
begin
329+
return sprintf('#<%s: {%s}>', self.class, to_a.inspect[1..-2])
330+
ensure
331+
ids.pop
332+
end
333+
end
334+
335+
alias to_s inspect
336+
337+
def pretty_print(pp) # :nodoc:
338+
pp.group(1, sprintf('#<%s:', self.class.name), '>') {
339+
pp.breakable
340+
pp.group(1, '{', '}') {
341+
pp.seplist(self) { |o|
342+
pp.pp o
343+
}
344+
}
345+
}
346+
end
347+
348+
def pretty_print_cycle(pp) # :nodoc:
349+
pp.text sprintf('#<%s: {%s}>', self.class.name, empty? ? '' : '...')
350+
end
351+
end
352+
private_constant :SubclassCompatible
353+
end

0 commit comments

Comments
 (0)