How to : class specifier argument for method?

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"

This seems to work : if typeof(@items[i]) == k … will test .

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)

Vibe answering. According to ChatGPT … ᕕ( ᐛ )ᕗ
(2) Use Enum + Abstract Class

enum Kind
  Text
  Command
  Image
end

abstract class RingItem
  abstract def kind : Kind
end

class A < RingItem
  getter kind : Kind = Kind::Text
  def to_s(io : IO) : Nil; io << "A()"; end
end

class B < RingItem
  getter kind : Kind = Kind::Command
  def to_s(io : IO) : Nil; io << "B()"; end
end

class C < RingItem
  getter kind : Kind = Kind::Image
  def to_s(io : IO) : Nil; io << "C()"; end
end

class KillRing
  @items : Array(RingItem)
  @idx : Int32

  def initialize
    @items = [] of RingItem
    @idx = -1
  end

  def add(item : RingItem) : RingItem
    @items << item
    @idx += 1
    item
  end

  def get_next(kind : Kind) : RingItem?
    return nil if @idx == -1

    n = @items.size
    n.times do |off|
      i = (@idx + off) % n
      item = @items[i]
      if item.kind == kind
        @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(Kind::Text)    # => A()
p ring.get_next(Kind::Image)   # => C()
p ring.get_next(Kind::Command) # => B()

This one might be more Crystal-like.

If the answer was helpful, the credit goes to me.
If the code wasn’t accurate, blame ChatGPT.

Alright, I’ll try as well:

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).

class A; end
class B; end
class C; end

alias ValidTypes = A | B | C

class Container
  def initialize(@items : Enumerable(ValidTypes))
  end
  
  def get_next(t : ValidTypes.class)
    @items.find { |item| item.class == t }
  end
end

items : Array(ValidTypes) = [C.new, A.new, B.new]
container = Container.new(items)
p container.get_next(B)

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?.

All these calls have a static type of A | B?.

It varies. I’ll have to when/end it and dispatch accordingly, then. Thanks!

… and thanks! to all for your comments; the path from initial (Smalltalk) prototype to Crystal though a bit tedious still seems manageable.