The Crystal Programming Language Forum

Syntax to call ancestor's virtual method explicitly

I have a class hierarchy and I want to be able to call a specific ancestor’s implementation of a method as I traverse the hierarchy. I am using super calls to move up the hierarchy, but then I want to be able to call the current @type’s implementation of another virtual method.

Here’s my example

class MachineState
  def on_entry
    # empty
  end

  def on_entry_recurse(old_state : MachineState)
    # empty
  end

  macro inherited
  # Entry starts below the top common state and progresses down
    def on_entry_recurse(old_state : MachineState)
      # stop once we reach a common ancestor
      if old_state.class > {{@type}}
        super
        on_entry  # calls virtual method - not what we want
        # maybe something like: {{@type}}::on_entry ??
      end
    end
  end
end

class S1 < MachineState
  def on_entry
    puts "In S1"
  end
end

class S11 < S1
  def on_entry
    puts "In S11"
  end
end

class S111 < S11
  def on_entry
    puts "In S111"
  end
end

describe "test it" do
  it "rescurses through ancestors" do
    s1 = S1.new
    s111 = S111.new
    s111.on_entry_recurse(s1) 
    # WANTED: In S11\n In S111
    # ACTUAL: In S111\nIn S111
  end
end

I didn’t fully read it, but try \{{@type}} (note the backslash)

perhaps a simpler example

class S1
   def my_method
     puts "S1"
   end
end

class S11 < S1
   def my_method
     puts "S11"
     S1::my_method # What is the proper syntax to call S1::my_method without using super
   end
end

I don’t this is something that is possible in any OOP language. You can’t just skip superclasses to call a specific one as it would break encapsulation. I’d consider thinking if you really need this, or could design things differently so you don’t.

At the least you could define dedicated protected methods in the hierarchy to do that but probably is a better way.

1 Like

This is possible in C++ which is only dubiously an OOP language example with explanation

I’ll come up with a macro to do it.

It is definitely possible in some languages. In Ruby you can do it through reflection:

S1.instance_method(:my_method).bind(self).call

But in Crystal, this is not possible. And I don’t think it should because this smells.

Can you tell us about the use case? I’m pretty sure there’s a better way to do things.

1 Like

I like how the first line in that explanation is:

Making a regular practice of overriding C++’s polymorphic facilities is not a good idea

Of course it’s possible in ruby ha. But yea, that deff smells.

Use case is described in initial post. Hierarchical state machine. I want to be able to define states with simple on_entry and on_exit methods. These should be called based on the transition between states. To do that, I need to recurse through the structure of states calling on_entry and on_exit as needed.

This could be solved with a macro, but I am learning Crystal and want to understand what is possible.

Thank you for the time!

But why do you need to call the method on an explicit ancestor? Why does traversing with super not work for this?

I would like to separate the traversal (kinda ugly and should be defined once) from the functionality.

If I use super, I have to put the traversal and its logic in every state’s on_entry and on_exit functions.

I would also need to define on_entry and on_exit for each state even if they do not need it. If I separate them the call from the traversal, I can have a default empty implementation of on_entry and on_exit in the base class that does nothing.

I’m not sure I see the problem, having super on the first line of each method is probably a lot more readable/easy to maintain than some complex macro to abstract that one word.

EDIT:

In those cases just don’t call super? NVM, misread

To be clear if you define them on the base type, then children only need to define them/call super if they need to do something specifically. Otherwise they would ofc inherit the behavior of the base type, i.e. noop.

It is more than just a super call. I need to check to see if we should stop the traversal. We stop at common ancestors. Further, even if one state does not define on_exit or on_entry, any parent might.

I’ll post the macro if I continue down this path. :D

1 Like

I think an example of a state machine and how you intend to use one would make this clear.

If you only want to define the traversal once on a particular superclass, why not make it a different method that isn’t obscured by subtypes?

class S1
  def my_method_S1
    puts "S1"
  end

  def my_method
    my_method_S1
  end
end

class S11 < S1
   def my_method
     puts "S11"
     my_method_S1
   end
end

In general, though, I agree that you should consider whether accessing a particular superclass implementation of an obscured method is really what you want.

Not sure if this is what you want, but doing {{@type}}.new.on_entry returns what you wanted :smiley:

https://play.crystal-lang.org/#/r/afqa