In my class, I’m using an Array to store union type A|B|C elements : @items = Array(A | B | C).new.
How should I declare a method that takes a class specifier argument (either of class A, B, or C) and returns the next array element of that class? The invocation would be akin to found = my_instance.get_next_item(C) .
I’ve tried something like def get_next_item(k : Class) : k ... but am getting a Error: unexpected token: "k".
If OTOH I declare the method thus : def get_next(k : Class) : A | B | C, I get a compilation error at the first invokation of if @items[i].is_a?(k) : Error: unexpected token: "k"
Nope. Compiles fine but nothing returned. EDIT: … which makes sense … typeof(item) returns (A | B | C) … i.e., runtime class instance introspection … does not exist ?
class KillRing
@items = Array(A | B | C).new
@idx = -1
def self.add(item : A | B | C) : A | B | C
@items.push(item)
@idx += 1
item
end
def self.get_next(k : Class) : A | B | C | Nil
return nil if @idx == -1
i = @idx
while i < @items.size
if typeof(@items[i]) == k
@idx = i
return @items[i]
end
i += 1
end
i = 0
while i < @idx
if typeof(@items[i]) == k
@idx = i
return @items[i]
end
i += 1
end
nil
end
end
Vibe answering. According to ChatGPT … ⸜( ᐙ )⸝
(1) Use generics.
class A; end
class B; end
class C; end
class KillRing
@items : Array(A | B | C)
@idx : Int32
def initialize
@items = [] of A | B | C
@idx = -1
end
def add(item : A | B | C) : A | B | C
@items << item
@idx += 1
item
end
def get_next(type : T.class) : T? forall T
return nil if @idx == -1
n = @items.size
n.times do |off|
i = (@idx + off) % n
item = @items[i]
if item.is_a?(T)
@idx = i
return item
end
end
nil
end
end
ring = KillRing.new
ring.add A.new
ring.add B.new
ring.add A.new
ring.add C.new
p ring.get_next(A)
p ring.get_next(C)
p ring.get_next(B)
The following code is simplified and focused solely on getting the next item of a specific type from a collection. It also uses an alias to group the data classes (which might be bad if there are a lot of them).
First the self. is unnecessary, since these methods are called on an instance of KillRing. If they are genuinely class methods then all the instance variables have to be changed into class variables, whose prefix is @@.
typeof returns the static type of its argument expression, thus it is a union because @items can store any of those elements. Runtime introspection is done through Object#class:
if @items[i].class == k
@idx = i
return @items[i]
end
While this works, the return type is still always A | B | C?, even if the argument is a single class. Storing the result of @items[i] to a variable does not help; this is a limitation of the language, so static type checks are preferred over runtime checks like this. #is_a? requires the type to be a constant, and this can be accessed via a free variable:
def get_next(k : T.class) : T? forall T
# ...
if (item = @items[i]).is_a?(T)
@idx = i
return item
end
# ...
end
The story does not end here. One might expect get_next(rand < 0.5 ? A : B) to pick either the first A or the first B at random; the argument type is A.class | B.class. On the other hand, get_next(A | B) would pick any A or B, whichever comes first; the argument type is (A | B).class. The above definition works for the latter case, with T = A | B, but not the former case, as the compiler currently does not dispatch calls to the same method instantiated with different free variables. If you really need to support both, you must list all overloads explicitly, and move the original body into a separate helper method:
private def get_next_impl(k : T.class) : T? forall T
# ...
end
def get_next(k : T.class) : T? forall T
get_next_impl(k)
end
# these overloads might be populated with macros
def get_next(k : A.class) : A?
get_next_impl(k)
end
def get_next(k : B.class) : B?
get_next_impl(k)
end
def get_next(k : C.class) : C?
get_next_impl(k)
end
Or you could just call it a day by making the caller write rand < 0.5 ? get_next(A) : get_next(B) instead. All these calls have a static type of A | B?.