Clay Allsopp

RubyMotion Metaprogramming

Sep 23, 2012

Metaprogramming is pretty neat. While there is an awfully fun mad-scientist feeling I get when my code creates and redefines methods and entire classes at runtime, metaprogramming is not for every programmer or every project. But when it is able to make your life easier, I'm very thankful it can be wrangled Ruby.

Now, conventional Ruby's flexibility and really-everything-can-change-at-runtime mentality come at a high cost with respect to performance; thankfully, RubyMotion isn't your father's Ruby. Its language is a compiled variant of Ruby that implements most of the standard library but without all of the slow interpreted cruft. Basically, this means we can't use eval, but RubyMotion still supports most of desktop Ruby's metaprogramming functions (we'll get to specifics soon, I promise).

Is it a huge deal to have this ability on iOS? The short answer is yes, assuming it floats your boat. Despite being built on top of good-ole C, Objective-C actually has some neat metaprogramming functionality tucked away in objc/runtime.h; however, it's not a first-class part of the language and isn't a go-to tool like the Ruby community has made method_missing or define_method.

So if you've always wanted more "meta" in your iOS code, let's jump into what RubyMotion does and does not support (hint: it supports everything important). In the meantime, I'll try to keep it real and give examples from actual code that I've written.

send

I don't know if you can count this as proper metaprogramming, but it does reveal one of the fundamentals of Ruby: methods can be invoked dynamically using just strings. Why is that a big deal? We can take a lot of if/elsif code and use send to simplify it into one call like so:

def tableView(tableView, didSelectRowAtIndexPath:indexPath)
    menu_row = self.menu_items[indexPath.row]
    # => 'profile'
    self.send("open_#{menu_row}")
end

def open_profile; #...; end

def open_messages; #...; end

def open_feed; # ...; end

Now when we add more rows to self.menu_items, we don't have to mess with didSelectRowAtIndexPath: at all! It scales with the rest of our product. Neat, right?

define_method

Since we can invoke methods dynamically, wouldn't it be nice if we could add them just the same? Well, you're in luck because define_method was just added to RubyMotion. With it, we can define (or re-define) a methods using a string and a block.

We can write part of the canonical has_one method from ActiveRecord in RubyMotion like so:

class Profile

  # EX: has_one :user
  def self.has_one(name)
    klass = make_klass_from_name(name)
    # EX: => User

    # Effect: (a_profile.user) === User.first(conditions: "profile_id = 4")
    define_method("#{name}") do
      klass.first(conditions: "#{self.class.stringify}_id = #{self.id}")
    end

    # Effect: (a_profile.user = a_user) === a_user.profile_id = 4
    define_method("#{name}=") do |value|
      value.send("#{self.class.stringify}_id=", self.id)
      value.save
      value
    end
  end

  def self.stringify; "profile"; end
end

Make sense? One confusing part might be that define_method appears to jump around in scope: when we define #{name}, our definition uses both the local klass variable from has_one and a reference to self.class. This is because define_method uses a normal Ruby block for its method definition, and thus is a proper closure. In less fancy words, it'll snatch local variables assigned outside of define_method and keep a reference to them in the definition for when the new function is actually called; everything else is evaluated in the scope of an instance of our Profile class.

Using generic variables like name and klass can make things hard to grok, so use define_method with caution. It can be incredibly powerful and speed up repetitive tasks, but can also make things a headache to debug.

You might have noticed we used a parameter in our block for #{name}=, which tells the definition that our function takes one argument. You might find a circumstance where you want to play with default values or pass a &block to a dynamically added method; for those, the normal do/end blocks will not suffice. Instead, use the stubby-lambda syntax, which allows every convention of a normal function definition:

# Stubby lambda: ->
define_method "#{name}_with_default", ->(arg1 = :my_arg, *args, &block) do
  p "arg1 #{arg1} args #{args.inspect}"
  block.call
end

define_singleton_method

Sometimes you want to add a method to a class, not an instance. For this, we use define_singleton_method, which functions just like define_method except acting on the class in question:

class Sayer
  def self.shouts(*words)
    words.each do |word|
      define_singleton_method "shout_#{word}" do
        p word.upcase
      end
    end
  end

  shouts :hello, :goodbye
end

Sayer.shout_hello
=> HELLO
Sayer.shout_goodbye
=> GOODBYE

For a more practical example, you might check out this gist by @senthilnambi.

alias_method

Pretty nifty method to copy current method definitions into a differently named function. Sometimes you do this to just change the function name, but other times you can use it to preserve an existing implementation and make use of it later.

class UIView
  alias_method :background_color, :backgroundColor
end

instance_eval

instance_eval shipped with the original version of RubyMotion and is particularly useful in the creation of DSLs. Any block you pass to it will be evaluated in the context of the object it is invoked on. Lots of big words in there, so check this out:

table_cell.addSubview(special_subview)

# alias_method is private, so have to use #send
table_cell.class.send(:alias_method, :old_layoutSubviews, :layoutSubviews)

table_cell.instance_eval do
  def layoutSubviews
    # nifty trick, right?
    old_layoutSubviews

    _special_subview = viewWithTag(SPECIAL_SUBVIEW_TAG)
    _special_subview.frame = #...
  end
end

Normally, if we wanted to add custom behavior in layoutSubviews we'd have to create a custom subclass. This might be appropriate, but in Formotion's case we often needed to apply changes to just one instance of a view.

instance_eval is especially powerful in creating DSLs. Usually DSLs will take a block supplied by the user and simply run instance_eval on it, like so:

class UIViewDSL
  def initialize(view, block)
    @view = view
    instance_eval(&block)
  end

  def background_color(color)
    @view.backgroundColor = UIColor.send("#{color}Color")
  end

  def frame(frame)
    @view.frame = frame
  end
end

def create_view(&block)
  view = UIView.alloc.initWithFrame CGRectZero
  UIViewDSL.new(view, block)
end

a_view = create_view do
  background_color :black
  frame [[0, 10], [100, 100]]
end

Note that because our &block in create_view is passed around, background_color and frame are actually invoking methods of UIViewDSL.

class_eval

So if instance_eval applies to an instance, how do we cascade changes to all instances? We can use class_eval on a Class and apply the same technique:

Fixnum.class_eval do
  def random
    rand(self)
  end
end

3.random
=> 1

module_eval

You get the idea.

const_get

const_get is how we grab classes, modules, and constants at runtime. The object on which we invoke const_get determines the scope of the results we get back; for example, grabbing a User class from a Facebook module versus a Twitter module.

I use this often to wrap the LongPrefixedObjectiveCConstants as symbols:

# EX const_int_get("UIReturnKey", :done) => UIReturnKeyDone == 9
def const_int_get(base, value)
  return value if value.is_a? Numeric
  value = value.to_s.camelize
  Kernel.const_get("#{base}#{value}")
end

# from the BubbleWrap/Camera source:
# @options[:source_type] is like :photo_library or :camera
const_int_get("UIImagePickerControllerSourceType", @options[:source_type])

The one downside to using const_get in RubyMotion is that all of the Objective-C integer constants must exist in your source code to get added at compile-time. For a good example, check the source to BubbleWrap's Camera module.

const_defined?

const_get will throw an error if we try to grab an undefined constant, so it might be a swell idea to run some type of safety check beforehand. const_defined? lets us do just that:

Kernel.const_defined? "UIView"
=> true
Kernel.const_defined? "UIAwesomeView"
=> false

const_set

If we can get and check for constants, then all that's left is to create them. Again, mind the scope at which you call this function; so far we've been using Kernel, which is the top-level scope, but you could accidentally override existing constants or classes.

Let's say we wanted to create a new subclass at runtime. That sounds pretty crazy, but call me maybe?

color = "green"
color_klass_str = "UI#{color.capitalize}View"
# => UIGreenView

if not Object.const_defined? color_klass_str
  Object.const_set color_klass_str, Class.new(UIView)
end

klass = Object.const_get color_klass_str

klass.ancestors
# => [UIGreenView, UIView, UIResponder, NSObject, Kernel]

klass.send(:define_method, "backgroundColor") do
  UIColor.send("#{color}Color")
end
# By default, our UIGreenViews will have green background colors.

And if you don't get goosebumps doing that, I don't know what thrills you.

What's not supported?

To my knowledge, only the string-based eval methods (which rely on an full-blown interpreter) and Bindings are not available in RubyMotion. With the recent addition of define_method, I'm even more excited to see what else comes out of the RubyMotion community.

If you have any questions or qualms, continue the discussion on Hacker News or send me a tweet.

Enjoy this? Follow for more on Twitter