Nick Kallen's blog
Ruby Pearls vol. 1 - The Splat
Over the next week or so I'll be sharing Ruby idioms and flourishes that I quite like. Today I'd I'll show a few tiny uses of splat! that make me tremble with delight.
Splat! - For Beginners
Splat! is the star (*) operator, typically used in Ruby for defining methods that take an unlimited number of arguments:
def sprintf(string, *args)
end
It can also be used to convert an array to the multiple-argument form when invoking a function:
some_ints = [1,2,3]
sprintf("%i %i %i", *some_ints)
Splat! - For Wizards
Array to Hash Conversion
The best use of splat! for invoking a infinite-arity functions I've ever seen is the recipe for converting an array to a hash. Suppose you have an array of pairs:
array = [[key_1, value_1], [key_2, value_2], ... [key_n, value_n]]
You would like to produce from it the hash: {key1 => value1 ... } You could inject down the array, everybody loves inject, but there is a better way:
Hash[*array.flatten]
Amazing right? This relies on the the fact that the Hash class implements the [] (brackets) operator and behaves thusly:
Hash[key1, value1, ...] = { key1 => value1, ... }
Heads or tails?
Splat! can be used for more than just method definition and invocation. My personal favorite use is destructuring assignment. I read this in Active Record's source code recently:
def sanitize_sql_array(ary)
statement, *values = ary
...
end
This is invoked when you do something like User.find(:all, :conditions => ['first_name = ? and last_name = ?', 'nick', 'kallen']). Splat! is used here is to get the head and tail of the conditions array. Of course, you could use always use shift, but the functional style used here is quite beautiful. Consider another example:
first, second, *rest = ary
One final trivium (#to_splat aka #to_ary)
You can actually customize the behavior of the splat operator. In Ruby 1.8, implement #to_ary and in 1.9 it's #to_splat. For example
class Foo
def to_ary
[1,2,3]
end
end
a, *b = Foo.new
a # => 1
b # => [2,3]
This also works for method invocation:
some_method(*Foo.new) == some_method(1,2,3)
When I first learned this at RubyConf I thought this was mind-blowing. I have since never used it.
alias_method_chain :validates_associated, :informative_error_message
I dislike the vague error message produced by validates_associated.
class User
validates_associated :profile
delegate ..., :to => :profile
end
I see the following error message: profile is invalid. But WHY was the profile invalid? The validation errors from the profile should bubble up to the user. So,
module ActiveRecord::Validations::ClassMethods
def validates_associated(association, options = {})
class_eval do
validates_each(association) do |record, associate_name, value|
associate = record.send(associate_name)
if associate && !associate.valid?
associate.errors.each do |key, value|
record.errors.add(key, value)
end
end
end
end
end
end
Now we see:
Music tastes can't be blank
Eh, voila!
Advanced Proxy Usage, Part I
One of the more underutilized features of ActiveRecord is the Assocation Proxy. But they are also one of the most powerful weapons in the ActiveRecord armory, and Rails apps that take advantage of them are better organized and easier to maintain.
What is a Proxy?
When in an ActiveRecord you declare an Association:
class Hand < ActiveRecord::Base
has_many :fingers
end
Instances of Hand now have a fingers method. Contrary to appearances, and contrary to the LIE told to you by hand.fingers.class, the fingers method does not return an Array of Fingers. Rather it returns a Proxy object, one that smells and tastes like an Array of fingers but actually has a rich creamy behavior all its own.
Scoped Access
The most basic use of Proxies is to "scope" the reading and writing of your ActiveRecords. For example, if you have a controller that allows CRUD on a User's Assets, you can read and write to the collection of Assets as in the following examples:
@asset = current_user.assets.find(params[:id])
@asset = current_user.assets.create(params[:asset])
@asset = current_user.assets.build(params[:asset]) # equivalent to 'new'-ing an object rather than 'create'-ing it.
There are other ways of doing this, of course:
@asset = Asset.create({:user => current_user}.merge(params[:asset))
But the Proxy Code is much better: not only is the Proxy code terse, but it meaningfully expresses the relationship between objects in your domain: Users have many assets; this Asset is created in the context of this User.
Special Queries (or Custom Finders)
The various Association declarations--has_many, belongs_to, etc.--allow you to express much more than a simple Foreign Key relation. We can richly express in the Proxy Declarations concepts like 'Assets that belong to a User' and 'Assets that don't belong to a User':
current_user.my_assets
current_user.other_assets
simply by declaring:
class User
has_many :my_assets, :class_name => 'Asset', :conditions => 'user_id = #{id}'
has_many :other_assets, :class_name => 'Asset', :conditions => 'user_id != #{id}'
end
Notice, in this last example, something peculiar: the use of single quotes ('') with variable substitution (#{...}). This is not a typo: the use of double-quotes, would perform variable interpolation when the has_many declaration is invoked. This is in the class-context--i.e., there is no instance yet. Rails always calls eval with a Binding of self when a call to one of the Proxy methods is performed, ensuring that this all comes together.
Let's consider an alternative to this approach: declaring finders as instance methods.
class User
def other_assets
assets.find(:conditions => ["user_id != ?", id])
end
end
What's wrong with this approach? Well, if you want to use this query in anything non-trivial--such as selecting the first ten of a User's Assets--you have to write fancy code:
def other_assets(options)
assets.find({:conditions => ...}.merge(options))
end
But good luck using this strategy to do pagination. You need to define my_assets and my_assets_count, too--have fun keeping your code DRY. With a proxy, we can just do something like:
current_user.my_assets.count
current_user.my_assets.sum
current_user.my_assets.average(:price)
In fact, all the richness of ActiveRecord class methods (and any other class methods of the Target type) are available to you here. Want to find all of a User's assets that are in State pending?
current_user.my_assets.find_by_state(State[:pending])
Another example:
class Asset
def self.find_portrait_assets
find(:all, :conditions => 'height > width')
end
end
Then,
current_user.my_assets.find_portrait_assets
returns only those portrait assets owned by a user.
Proxy Options
Proxy declarations accept a number of interesting parameters. There are even "lifecycle" callbacks, like after_add, and before_destroy just like a normal ActiveRecord has before_create and so forth. You can hook into these by using an option.
class User
has_many :assets, :after_add => [:send_email] do
end
def send_email(r)
end
end
This after_add could be defined in the Asset class. But suppose Assets had a Polymorphic association. Both Users and Articles have many Assets. Our Business Rule is only to send email when a User adds an Asset, not an Article. We could write:
class Asset
def after_create
case owner
when User
# send email
when Article
end
end
But this is clumsy! When we have logic to express about the relationship between things, the Proxy is the right place for it. Anywhere else is just smearing logic throughout your code.
Proxy Extensions
Consider the following example:
has_many :assets do
def to_s
self.join(',')
end
end
You can actually extend your Proxy Objects with an Anonymous module! When you have logic that applies to a Collection of ActiveRecords, your has_many Proxy is probably the proper place for it. For example:
class Table
has_many :cells do
def to_matrix
# convert from list to matrix form.
end
end
end
Another example of this technique is the following. Suppose an Asset as many Versions, such as small, medium, etc. We'd prefer a shorter way of finding the proper version of an Asset than saying asset.versions.find_by_name('thumbnail'), we'd like to just say asset.versions[:thumbnail]. Just define the brackets ([]) operator on the Proxy:
class Asset
has_many :versions, :class_name => 'Asset', :foreign_key => :parent_id do
def [](version_name)
find_by_name(version_name)
end
end
end
Suppose we want to go one step further. If a particular version doesn't exist, it shall be created on-the-fly:
def [](version_name)
if version = find_by_name(version_name)
version
else
# create a new version here.
end
end
Advanced Extensions
In some cases, we want to write generic Extensions--these should work regardless of the particular classes involved. In the context of a Proxy there are three methods you should be aware of: proxy_owner, proxy_target, and proxy_reflection.
Suppose we want to implement something like the build method, but one that doesn't have the side effect of adding it to the owner in memory:
has_many :foo do
def new(options = {})
proxy_reflection.klass.new({proxy_reflection.primary_key_name => proxy_owner.id}.merge(options))
end
end
Extensions are so useful--it just requires a little imagination--that I'm going to give one more example, this one apropos of Access Control:
class User
has_many :draft_articles do
def readable_by?(user)
user == proxy_owner
end
end
end
Some Miscellany
The
has_oneandbelongs_toProxies behave a bit oddly: here,cyclops.build_eyeis used rather than the more obviouscyclops.eye.build.In general,
has_oneandbelongs_towill shadow methods on the Target. Don't name any database columnstargetorowner, for instance. This is one of the biggest complaints against the current implementation of Proxies!Both build and create will work even if the Proxy Owner is new. For example,
u = User.new; u.assets.build; u.save. In this example, both objects will be saved with the Foreign Key set correctly.Both build and create can take an array of attributes hashes. For example:
u.assets.build([{...}, {...}]). This will build two assets at once. (This is quite nice where in a Controller you have a form that allows the upload of multiple Assets at once. The Controller code looks identical (in simple cases) regardless of whether the form allows a single or multiple upload!)
That's the basic idea. In part II of this Article (to be released in the coming weeks), I will discuss 'static' Proxy methods and I will release version 0.1 of a new plugin that builds upon a lot of exciting work in this area. In the meantime, check this out.







