Ruby shenanigans - Turning instance methods into class methods

2022-08-10

Warning: dark Ruby metaprogramming magic ahead

TL;DR: It is possible to use instance methods in case statements:

3.0.0 :001 > 4.even?
 => true 
3.0.0 :002 > case 4
3.0.0 :003 > when even? then p "works"
3.0.0 :004 > when odd? then p "wat"
3.0.0 :005 > else raise Error
3.0.0 :006 > end
Traceback (most recent call last):
        4: from /home/rafael/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `<main>`
        3: from /home/rafael/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `load`
        2: from /home/rafael/.rvm/rubies/ruby-3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>`
        1: from (irb):3:in `<main>`
NoMethodError (undefined method `even?` for main:Object)
3.0.0 :007 > Integer.instance_methods.map { |m| Integer.instance_method m } \
                                     .filter { |m| m.arity == 0 } \
                                     .each { |m| Integer.define_singleton_method(m.name) { -> (it) { m.bind_call(it) } } }
 => [#<UnboundMethod: Integer#next()>, #<UnboundMethod: Integer#-@() <internal:integer>:6>, <snip>...]
3.0.0 :008 > case 4
3.0.0 :009 > when Integer::even? then p "works"
3.0.0 :010 > when Integer::odd? then p "wat"
3.0.0 :011 > else raise Error
3.0.0 :012 > end
"works"
 => "works"

And if you want this as a gem, I present you Singl.


So, imagine you have a class, that has a bunch of (instance) methods without arguments (like is_even? in the Integers example above )…


class Foo
  ...
  def is_bar?
    frob and fib(10) > 4
  end

  def is_baz?
    bat > 100
  end

  def is_frob?
    frob.to_b and self.logic
  end
end

And now you would like to use a case to differentiate when a Foo is a bar, baz, or frob. You might want to do something like this:

case res.method_that_returns_a_foo(frob, nicate)
when is_bar? then ...
when is_baz? then ...
when is_frob? then ...
else raise Error
end

So, this doesn’t work, because we are just calling is_bar? in the void, without any receiver object. The proper way to write this is to define a temporary variable, and then call the methods on it:

foo = res.method_that_returns_a_foo(frob, nicate)

case
when foo.is_bar? then ...
when foo.is_baz? then ...
when foo.is_frob? then ...
else raise Error
end

Well, that repetition is tiring. So the question is: can we not have the temporary variable? The answer is yes, but this is quite convoluted. It involves (click the links to jump to the relevant section):

The end result is this being possible:

case res.method_that_returns_a_foo(frob, nicate)
when Foo.is_bar? then ...
when Foo.is_baz? then ...
when Foo.is_frob? then ...
else raise Error
end

I ended up packaging these experiments in what is my first gem - I named it Singl (and used the opportunity to get more Ruby packaging experience). Here is the code, enjoy (and contributions are welcome)!

Instance methods vs Singleton methods

So, a bit of terminology first: in Ruby, “singleton methods” are the real name used for “class methods” (aka “static methods” in other languages). What this means in practice is that the method is not defined for the instances of the class (or module), but to the class itself. In code:

class Foo
  def self.bar
    :singleton
  end
  def baz
    :instance
  end
end

res = Foo.new

Foo.bar
res.baz

Trying to do res.bar or Foo.baz results in a NoMethodError. Now, imagine you have a class with a lot of instance methods defined – how to turn them into singleton methods, that then receive the intended receiver in the arguments? In code:

class Foo
  def baz
    self.logic and :instance
  end

  def self.baz(foo)
    foo.logic and :instance
  end
end

And here is where .bind_call comes into play.

.bind_call

So, I discovered .bind_call when trying to work around some nasty bit of a codebase. Sumarizing, it is possible to have “unbound” instance methods (UnboundMethods) that then are bound to whatever object you want (via .bind), and then you can call those methods as normal. And you can do this at runtime; the only restriction is that the bounded object actually is_a? object of that type (or a subtype), otherwise you get a TypeError.

And you can get a complete list of instance methods from a given class (or module) via .instance_methods. Unfortunately, that only gives the names of the methods (as symbols); to actually get an UnboundMethod it is needed to call .instance_method with the retrieved method names.

Finally, it is possible to define new singleton methods programatically via .define_singleton_method.

So, putting it all together, to automatically do the transformation presented above, we need to: - get instance methods via .instance_methods - define new singleton methods + that will bind the instance method to the first object passed as an argument

Roughly, that translates to:

klass.instance_methods
     .map { |m| klass.instance_method m }
     .each do |m|
       klass.define_singleton_method(m.name) { |selv| m.bind_call(selv) } }
     end

With this snippet, you can do interesting things, like having Integer.odd? defined. However, this is still not good enough to use in a case expression.

Case expressions, and the difference between calling Procs and Methods

As a beginner in Ruby, coming from Python and C, one of the things that are really different is the different way of handling functions (like passing as argument and assigning them to variables) without just calling them. Most languages end up having the convention that to call them you just need to open and close parenthesis (eg foo.bar(arg)) and without them, it is just an object or value (eg foo.bar). Ruby doesn’t use parenthesis to signify calling the method, so you end up identifying methods by just their name (:bar). If want to get the callable object, you have to do something like callable_bar = foo.method :bar. And to actually call it afterwards, you’ll need to do callable_bar.call args. And if you create a Proc or a lambda, you’ll also have to use .call.

In practice, this means that if you try to do

case foo
when Foo.bar then :ups
when Foo.baz then :wat
end

is that Foo.bar will be immediately called, which is not what we want.

So, looking more into the Ruby documentation on case expressions, we can see that whatever is specified in the when expression gets called with the === method using as argument what is in the case. And one of the strange things I learned as a Ruby begginer is that .=== is actually a .call in lambdas and Procs. This means that we need to return a lambda that can then finally evaluate the method with a .bind_call on the received argument. This is done in the first code snippet of this post – the ->(it) { m.bind_call(it) } is creating a lambda that receives 1 argument (it), that then is used to the .bind_call.

But if we return a lambda, then we won’t be able to just do Integer.even? 4, we’ll have to do Integer.even?.call 4. Unless… we see if any argument is passed – if so, then immediately call the method, otherwise return the lambda to defer evaluation. Then, we get the best of both world: call instance methods as singleton ones and if the necessary argumentes are missing (remember we need at least one to use as the receiver).

Bonus: using .send instead of .bind_call

Later it dawned on me that I could have done this just using .send on the object instead of .bind_call, and I ended up trying that out. It is really simple – the following two lines of code are the difference

m.bind_call it
it.send m.name

It is more like dynamic Ruby, as it uses the sending of messages as method calls instead of restricted to a specific method.

> Singl.even? 1
false
> Singl.even? :a
NoMethodError

It has advantages if the case statement needs to be polymorphic – as the .bind_call approach uses a method out of a specific class, and the .send approach only restriction is that the method must be defined (aka duck typing). For example, Integer.zero? 0.0 will fail due to TypeError, while Singl.zero? 0.0 will work (as it is equivalent to 0.0.zero?).