RubyMotion Metaprogramming
Sep 23, 2012Metaprogramming 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 Binding
s 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.