Sandwich Scheduling: a look at Ruby's Array cycle method
- By Tim Ash
- 28 September
How Ruby can help you schedule your sandwiches
An example of Ruby metaprogramming, by way of juggling chainsaws.
Photo by Sandro Ayalo on Unsplash
Metaprogramming means writing code that writes code itself. In Ruby, this means objects, classes, and methods can be created and modified during runtime.
I’m going to illustrate some of Ruby’s metaprogramming features using the example of a program which creates methods on an existing object as and when they are required.
Imagine we have been tasked to write a program to keep track of who was the first person to do something silly (eg juggling chainsaws) in an inappropriate location (eg Machu Picchu). Specifically, a user should be able to ask a particular location if they were the first person to perform a particular activity there. If so, they’ll get a message of congratulations. If not, they’ll be told which user was the first person to perform that activity in that place.
There are a number of different approaches to this problem, but we’re going to explore a metaprogramming solution. Let’s start by defining some very simple classes:
class User
attr_accessor :name
end
class Activity
attr_accessor :description
end
class Location
attr_accessor :name
end
We have a class to describe users, which allows us to set and retrieve the user’s name, a class to describe the activities, which allows us to set and retrieve the activity’s description, and a class to describe the location, which allows us to set and retrieve the location’s name. We can create objects of these classes as follows:
bob = User.new
bob.name = "Bob Smith"
juggle_chainsaws = Activity.new
juggle_chainsaws.description = "Juggle Chainsaws"
machu_picchu = Location.new
machu_picchu.description = "Machu Picchu"
However, we don’t yet have any code to associate these objects with each other, which will be required if we want to track the fact that Bob Smith was the first user to juggle chainsaws at Machu Picchu. We’re going to implement this using the method_missing method on the Location class:
class Location
attr_accessor :description
def method_missing method, *args, &block
return super method, *args, &block unless method.to_s =~ /^was_i_the_first_person_to_\w+/
what = method.to_s.gsub('was_i_the_first_person_to_', '').tr('_', ' ')
where = self.description
who = args[0].name
puts "Congratulations! You are the first person to #{what} at #{where}."
self.define_singleton_method method do |*args|
puts "The first person to #{what} at #{where} was #{who}."
end
end
end
There’s a bit going on here: the method_missing method is called when a method is called on an object, but the object cannot find a method with that name. In this case, we only want to intercept methods whose names start with the string “was_i_the_first_person_to”. Any other unrecognised methods will be referred to the superclass, where they will probably result in an error.
Let’s assume the following call was made:
machu_picchu.was_i_the_first_person_to_juggle_chainsaws bob
Our method name starts with “was_i_the_first_person_to_”, so we now determine what was done by extracting the string “juggle_chainsaws” from the method name. The location already has a description, and who performed the activity is passed in as an argument.
Using this information, we print a message of congratulations, and then define a singleton method on the machu_picchu object called was_i_the_first_person_to_juggle_chainsaws, which will print a string letting us know who the first person to juggle chainsaws at Machu Picchu was.
Our output will look like:
> machu_picchu.was_i_the_first_person_to_juggle_chainsaws bob
"Congratulations! You are the first person to juggle chainsaws at Machu Picchu."
Now imagine another user, wondering if he was the first person to juggle chainsaws at Machu Picchu, calling this function:
> billy = User.new
=> #<User:0x0000560f8c68ccb0>
> billy.name = "Billy Jones"
=> "Billy Jones"
> machu_picchu.was_i_the_first_person_to_juggle_chainsaws billy
=>"The first person to juggle chainsaws at Machu Picchu was Bob Smith."
From now on, all calls to was_i_the_first_person_to_juggle_chainsaws on the machu_picchu object will ignore method_missing, as the method now exists, and the method will print the string: “The first person to juggle chainsaws at Machu Picchu was Bob Smith”.
As a bonus it turns out that we don’t need a separate Activity class, as the method_missing method in the Location class infers this from the method name, so we can get rid of it entirely, leaving us with:
class User
attr_accessor :name
end
class Location
attr_accessor :description
def method_missing method, *args, &block
return super method, *args, &block unless method.to_s =~ /^was_i_the_first_person_to_\w+/
what = method.to_s.gsub('was_i_the_first_person_to_', '').tr('_', ' ')
where = self.description
who = args[0].name
puts "Congratulations! You are the first person to #{what} at #{where}."
self.define_singleton_method method do |*args|
puts "The first person to #{what} at #{where} was #{who}."
end
end
end
In the real world, meta-programming is widely used when creating Domain-Specific Languages (DSLs). For example, FactoryBot is a library which provides a clear and simple interface for setting up test data, which relies heavily on metaprogramming. It’s worth taking a look at their code: https://github.com/thoughtbot/factory_bot
A word of caution, however. Code which makes extensive use of metaprogramming can be hard to debug, not least because instead of trying to work out why the code you’ve written doesn’t do what you want it to, you’re trying to work out why the code written by the code you’ve written doesn’t do what you want it to.
I’d love to hear your thoughts on metaprogramming in Ruby, and the uses you’ve found for it. Why not leave a comment below?
How Ruby can help you schedule your sandwiches
Another way Ruby can help you keep track of your lockdown exercise routine
How Ruby can help you keep track of your lockdown exercise routine
How Ruby can help you mix the drinks and bust some moves at the charity ball
You won't believe what happens when all the animals run away from the zoo.
Home, home on the Range, where the Ewoks and the Wookies play
We would love to hear from you so let's get in touch!