Monday, October 18, 2010

Hash#in_groups_of

Here is a quick and dirty snippet to distribute a Hash into an Array of mini-Hashes as an analogue to the Rails Array#in_groups_of method.


module MyHashExtensions
def in_groups_of(*args)
to_a.in_groups_of(*args).inject([]) do |accum, group|
accum << group.inject({}) {|acc, pair| pair.nil? ? acc : acc.merge(pair.first => pair.last)}
end
end
end
end

Hash.class_eval do # jam this into Hash however you wish!
include MyHashExtensions
end


Example usage:

{:a => 'b', :c => 'd', :e => 'f', :g => 'h', :i => 'j', :k => 'l', :m => 'n'}.in_groups_of(3)


gives...


[{:i=>"j", :e=>"f", :g=>"h"}, {:c=>"d", :a=>"b", :k=>"l"}, {:m=>"n"}]


I used this for Cassandra/Rails/Thrift experimentation today! Remember, the text buffer is your canvas. You are a software artist, whether you like it or not. Play with the code, and if someone doesn't like it, throat punch them.

Wednesday, January 13, 2010

Undoing Alias Method Chaining

This is more of a quick rant on AliasMethodChain in Rails than a tutorial... Metaprogramming in Rails is powerful. It is a wonderful way to build onto a framework which itself is built on top of Ruby via metaprogramming. I've seen and heard many arguments describing metaprogramming as the root of all evil. The chaos that it can cause when not structured is a terrible thing to behold, certainly.

I won't say much about how we handle our system extensions at Groupon, except, that it is very structured with a centralized place to discover all of the extensions to Rails internals in a single place (expect a more formal description of our process later from our Team!). Also, the extension points that we chose to use have been the parts of Rails which are the least subject to change (and have demonstrated the greatest resiliency and consistency over the last 3-4 years of active development).

Anyway, this morning, I ran into one of the (very few) cases where testing truly required assertions at various points in an alias_method_chain stack. As you can imagine, if you are alias_method_chaining ActiveRecord::Base#find, for example, you'll most likely have architected your system in such a way, that every link in the chain is standard, unalterable behavior (we absolutely do not muck with #find, btw. It is already an atrocious method). For these cases, I employ this little hack...


ActiveSupport::CoreExtensions::Module.class_eval do
def alias_method_chain_unlink(target, feature)

# Strip out punctuation on predicates or bang methods since
# e.g. target?_without_feature is not a valid method name.
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
yield(aliased_target, punctuation) if block_given?

with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"

alias_method target, without_method

case
when public_method_defined?(without_method)
public target
when protected_method_defined?(without_method)
protected target
when private_method_defined?(without_method)
private target
end
end
end

Notice its a natural inversion of 'alias_method_chain' implemention. This method would probably never fly with the core team, so it is merely a re-implementation/repurposing rather than an attempt to DRY the aliasing code.


As an example of what I'm talking about, imagine that you have this chain..

class User
def cry
puts "cry"
end
end

User.new.cry
User.class_eval do
def cry_with_emotion
puts "WAH WAH WAH"

cry_without_emotion
end

alias_method_chain :cry, :emotion
end

User.new.cry



Let us say that you want to alter the emotive nature of that extension in the same ruby session

User.class_eval do
def cry_with_emotion # slightly less emotive
puts "Wah Wah Wah"

cry_without_emotion
end
end

User.new.cry

User.class_eval do
define_method :cry_with_emotion do
puts "Wah Wah Wah"
end
end
User.new.cry # no luck again eh??


As you can see, redefining an interstitial method does not automatically make it part of the chain! Try this on for size (after redefining your method).


User.class_eval do
alias_method_chain_unlink :cry, :emotion

alias_method_chain :cry, :emotion
end
User.new.cry


That works! If you can imagine it, this hack can serve to either allow you to remove something out of a long alias_method_chain, or, allow redefinition of chain link (for lack of a better term)! I might have more to say about metaprogramming in Ruby later, but for now, may this approach save someone as much time as it saved me!