I stumbled upon a problem with one of my scripts: how do you split a yielded block between two different callers while conforming to the DRY principe? The answer is lambda method.
I have a rather big database with multiple interconnected tables. I needed a script that parses this database and outputs a result for some query. Obvious answer is to use ActiveRecord for this. So I did. But the problem arose when I added pagination so that my script does not explode in memory. I needed at the same time:
- limit the memory usage with pagination
- preserve the option to limit query with SQL LIMIT command, which pagination ignores.
Lambda to the rescue
Fortunately I was aware of the ruby’s lambda method, though this would be probably the very first time I actually needed it. The answer is to create a lambda object and give it to the yielding method instead of a block:
# just a simple processor for a ActiveRecord record proc_book = lambda{|book| # here we process each record puts book.name } conds = ["books.name is like ?", "ruby"] # Tables to be eager-loaded include_tables = [:author, {:reviews => :author}] # if limit is given and it is within reasonable size, use it if limit && limit > 0 && limit < 5000 Book.find(:all, :conditions => conds ,:limit => limit, :include => include_tables).each(&proc_book) else # Ignore limit and hook up will_paginate WillPaginate::enable_activerecord Book.paginated_each(:conditions => conds, :include => include_tables, &proc_book) end
This script is of course a simple proof of concept, but it serves the point and is derived from a real world usage. Note also the referenced conds and include_tables variables, to be even more DRY.
I think this is an example of bad programming, regardless of whether it’s a proof-of-concept or not. You have an if/else statement where both execution branches perform a kind of paginated query, only using different methods.
Lambdas have their uses, but not to DRY up code that was overcomplicated in the first place.
LikeLike
I can see your point, but the foremost reason for this post was to show how the same yielded block can be used in multiple places (for people who missed it in the Pickaxe).
The actual code is such because my existing code provides an option to limit the number of rows returned and I could not change that. But at the same time iterating over all records blew out of memory very quickly so that pagination was needed.
Yes, I admit that I was in a bit of hurry and did not read will_paginate API docs thoroughly, but to my testing it simply ignored the :limit and I could not find an alternative to limit the total result set while using the paginate call.
And as pagination is a wrapper layer above core find, I do not see a problem in dropping that layer when not needed while using lambda to keep it DRY.
You are welcome to explain where I went wrong.
LikeLike
It can be used for dynamic ActiveRecord callbacks meaning that you can pass the proc ref to the model instance to be processed on any callback. In general it can look like this.
proc_after_save = lambda{|book|
puts book.name
}
@book = Book.find(1)
@book.set_callback(proc_after_save)
class Book << ActiveRecord::Base
@extra_callbacks = []
before_save :run_callback
def run_callback
@extra_callbacks.each do |p|
p.call
end
end
def set_callback(proc_ref)
@extra_callbacks << proc_ref
end
end
But in general, it looks way too Perlish
LikeLike