tscheingeld
tscheingeld

Reputation: 861

Metaprogramming: method that works like "def"

I'd like to define a singleton method for a class object that works just like def but also does some other stuff. Inheriting classes can use that method like def without having any other special syntax other than using the method name rather than def.

A concrete example will help. In the code I'm developing I want to mark certain methods as "alpha" or "beta". That way you can query a class object for which methods are normal, which alpha, and which beta. Here's how I do it currently:

# Person
class Person
  def self.states
    if not instance_variable_defined?(:@states)
      @states = {}
     end

     return @states
  end

  def self.set_state(method_name, state)
    states[method_name] = state
  end
end

# Guest
class Guest < Person
  set_state 'sender', 'alpha'

  def sender(param1, param2)
    # do stuff
  end
end

puts Guest.states

That last line outputs

{:sender=>"alpha"}

OK, that works. But what I'd really like is to be able to call a method that works just like def, but which can flag a method as alpha or beta. So the Guest class might look like this:

class Guest < Person
  alpha sender(param1, param2)
    # do stuff
  end
end

I'd rather not use a block, like as follows. Setting aside scoping and other issues, it just doesn't feel like it's defining a method.

alpha('sender') do |param1, param2|
  # do stuff
end

Is there a way to metaprogram such a thing?

Upvotes: 0

Views: 68

Answers (2)

Simple Lime
Simple Lime

Reputation: 11050

Instead of attempting to do this with def, you could instead do this by emulating public/private/protected, something like:

module MethodState
  def states
    @states ||= {}
  end

  def current_state
    @current_state ||= :normal
  end
  private :current_state

  [:alpha, :beta, :normal].each do |method_state|
    define_method method_state do |*method_names|
      if method_names.empty?
        @current_state = method_state
      else
        method_names.each { |name| states[name] = method_state }
      end
    end
  end

  def method_added(method_name)
    states[method_name] = current_state
  end

  def self.extended(base)
    # all already defined methods are 'normal' state methods
    base.normal *base.instance_methods(false)
  end
end

class Guest
  def normal_method_one
  end

  def normal_method_two
  end

  extend MethodState
  alpha
  def sender(p1, p2)
  end

  beta
  def a_beta_method
  end

  def another_alpha_method
  end
  alpha :another_alpha_method

  normal
  def one_last_normal
  end
end

Guest.states # => {:normal_method_two=>:normal,
# :normal_method_one=>:normal, :sender=>:alpha,
# :a_beta_method=>:beta, :another_alpha_method=>:alpha,
# :one_last_normal=>:normal}

Upvotes: 2

Chris Heald
Chris Heald

Reputation: 62668

You can't quite do what you want, because alpha sender(...) attempts to pass the value of sender through to the alpha method. This obviously can't work.

However, method definition does return the symbol name of the defined method, which you can then pass to your alpha method:

class Person
  def self.alpha(method_name)
    set_state method_name, "alpha"
  end
  # ...
end

class Guest < Person
  alpha def sender(param1, param2)
    # do stuff
  end
end

This will define sender and call alpha(:sender), which will then invoke set_state appropriately.

Upvotes: 2

Related Questions