gx

Learn Ruby - Callback Functions

Posted at — Sep 29, 2023

The main programming language in my new job is Ruby. Thus I have to spend some time learning it. As a Go and Python programmer, some features in Ruby really shock me. I have it recorded here.


I am an advocate of typing. As there are type hints in Python, I think some similar hints may be in Ruby. The most popular ones seem to be the offical rbs and the Sorbet,

The syntax of rbs is similar with Python’s. However, rbs has to be written as separate files and it only works as hints.

module ChatApp
  VERSION: String

  class User
    attr_reader login: String
    attr_reader email: String

    def initialize: (login: String, email: String) -> void
  end
end

Sortbet supports type checking. However, the syntax confused me.

# typed: true
extend T::Sig

sig {params(name: String).returns(Integer)}
def main(name)
  puts "Hello, #{name}!"
  name.length
end

I mean, the syntax is very clear and easy to integrate it into codebase. However, I could not figure out how the sig worked. How does a function call affects a following function definition? In Python, decorators works the same way but it is defined by a specific syntax instead of a simple function call.

Let’s investigate how sig works.

module T::Sig
  def sig(arg0=nil, &blk)
    T::Private::Methods.declare_sig(self, Kernel.caller_locations(1, 1)&.first, arg0, &blk)
  end
end

module T::Private::Methods
  def self.declare_sig(mod, loc, arg, &blk)
    T::Private::DeclState.current.active_declaration = _declare_sig_internal(mod, loc, arg, &blk)

    nil
  end

  private_class_method def self._declare_sig_internal(mod, loc, arg, raw: false, &blk)
    install_hooks(mod)

    # ... ...
  end

  def self.install_hooks(mod)
    # ... ...

    if mod.singleton_class?
      mod.include(SingletonMethodHooks)
    else
      mod.extend(MethodHooks)
    end

    # ... ...
  end

  module MethodHooks
    def method_added(name)
      super(name)
      ::T::Private::Methods._on_method_added(self, name, is_singleton_method: false)
    end
  end
end

We can find an interesting function name, method_added. What will happen if we extend a class with a method_added function?

module Hook
  def method_added name
    super name
    puts "method_added is called with parameters #{name}"
  end
end

class Foo
  extend Hook

  def foo; end
end

After running the code above, “method_added is called with parameters foo” will be printed.

Now it is clear how the sig checks signatures of functions in runtime:

There are some other callback functions in Ruby, like included, extended, prepended, inherited, method_missing, which would be helpful when implementing complicated features.

I was quite impressed by those callback methods in Ruby, which indicate that the behavior of the program is generated in runtime instead of in compile time. It is very different from other languages.