I love “map by pluralisation” [now: map_by_method]
Update: this is a gem called map_by_method.
The other day I introduced a new syntax idea that I call “map by pluralisation”. Everyday I use it in code and in the console/irb I fall more in love with its simplicity - both to type and to read.
The following are all equivalent:
>> BankTransaction.columns.names # map by pluralisation => ["id", "amount", "date", "description", "balance"] >> BankTransaction.columns.name # singular works too => ["id", "amount", "date", "description", "balance"] >> BankTransaction.columns.map_name # merge collector (map) + operation (name is method of BankTransaction object) => ["id", "amount", "date", "description", "balance"] >> BankTransaction.columns.collect_name # merge collector (collect) + operation => ["id", "amount", "date", "description", "balance"]
All of which are easier to read and quicker to type than the current equivalents:
>> BankTransaction.columns.map {|p| p.name} # standard map => ["id", "amount", "date", "description", "balance"] >> BankTransaction.columns.map &:name # Symbol.to_proc => ["id", "amount", "date", "description", "balance"]
You can now type:
>> BankTransaction.columns.select_primary # merge collector (select) and operator (primary returns true/false on a Column object) => [#<...MysqlColumn:0x3c8a4f0 @limit=11, @sql_type="int(11)", @primary=true, @type=:integer, @number=true, @name="id">]
Instead of:
>> BankTransaction.columns.select {|c| c.primary} >> BankTransaction.columns.select &:primary
Use with ActiveRecords
“Map by pluralisation” is truly wonderful in the console for exploring and collecting data models.
Without knowing a thing about the data model, I bet you can understand either of the following:
@transactions = BankTransaction.find :all, :conditions => ['date = ?', Date.today], :include => [:accounts => [:owner]] return @transactions.collect_accounts.select_overdrawn?.collect_owner.full_names
Answer: the full name of each owner of an overdrawn bank accounts for all today’s bank transactions.
It reads well as there is a minimum of {, }, |, &, : characters.
Download
Tip for use with ActiveRecord Associations
You may need to cast associations to Arrays.
@account.transactions.to_a.map_dates
The result of the transactions assocation on the Account class is not an Array. My initial attempts to provide the “map by pluralisation” code (a method_missing) didn’t work, and casting it to an explicit Array using to_a seemed simple and harmless.
Discussion of Syntax Ideas
New magical version of Symbol.to_proc
Before the magic, let’s go through a Beginner’s Guide to Mapping, then Advanced Guide to Symbol.to_proc, and THEN, the magical version. Its worth it. Its sexy, nifty AND magical all at once.
Beginner’s Guide to Mapping
Need to invoke a method on each object in an array and return the result?
# call ‘to_i’ on each item of the list to return the list of numbers >> list = ['1', '2', '3'] => ["1", "2", "3"] >> list.map {|item| item.to_i} => [1, 2, 3]
That is, we called the to_i method on each item of the list, thus converting the 3 strings into 3 integers. Of course, we could have called any method on any set of objects.
Advanced Guide to Symbol.to_proc
After doing that a few times, you start wishing there was simpler syntax. Enter: Symbol.to_proc
>> list.map &:to_i
=> [1, 2, 3]
It looks like magic. Well it certainly doesn’t make any sense. But it works. (The secret: the :to_id symbol is cast into a Proc object by the leading &. That is, it calls the to_proc method on the symbol. Rest of explanation here.)
Magical version of Symbol.to_proc
Quite frankly, that’s still a lot of syntax. Plus, I normally forget to added parentheses around the &:to_i, and then latter I want to invoke another method on the result, so I need to add the parentheses which is a pain… anyway. I thought of something niftier and dare I say, more magical.
How about this syntax:
>> list.to_is
=> [1, 2, 3]
This syntax makes sense - to_is is the plural of to_i - its obvious you want the to_i’s of your list.
What’s happening here?You pass the plural of the method name to the container and it invokes the singular of the method name upon the children using the map code above. Source is below.
More examples? Want to get the names of all the objects of a list? (assuming that each item in the list has a name method)
>> contacts.map {|contact| contact.name}
=> ['Dr Nic', 'Banjo', ...]
>> contacts.map &:name
=> ['Dr Nic', 'Banjo', ...]
>> contacts.names
=> ['Dr Nic', 'Banjo', ...]
You pick your favourite. I love the last one.
Bonus examples:
>> (1..10).to_a.to_ss => ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] >> (1..10).to_a.days => [86400, 172800, 259200, 345600, 432000, 518400, 604800, 691200, 777600, 864000] >> [2,'two', :two].classes => [Fixnum, String, Symbol] >> [2,'two', :two].classes.names => ["Fixnum", "String", "Symbol"] >> [2,'two', :two].classes.names.lengths => [6, 6, 6]
So much happy syntax in one place!
UPDATE: After conversation on Ruby Forums, we’ve come up with some new syntax:
>> (1..5).to_a.map_days => [86400, 172800, 259200, 345600, 432000] >> list = [nil, 1, 2, nil, 4] => [nil, 1, 2, nil, 4] >> list.reject_nil? => [1, 2, 4]
Neat.
How do I do this at home?
Download and include this mini-library. Source below. Have fun.
module GenericMapToProc
def self.included(base)
super
base.module_eval <<-EOS
def method_missing(method, *args, &block)
super
rescue NoMethodError
error = $!
begin
re = /(map|collect|select|each|reject)_([\\\\w\\\\_]+\\\\??)/
if (match = method.to_s.match(re))
iterator, callmethod = match[1..2]
return self.send(iterator) {|item| item.send callmethod}
end
return self.map {|item| item.send method.to_s.singularize.to_sym}
rescue NoMethodError
nil
end
raise error
end
EOS
end
end
unless String.instance_methods.include? "singularize"
String.module_eval <<- EOS
def singularize
self.gsub(/e?s\Z/,'')
end
EOS
end
# Add this to the Array class
Array.send :include, GenericMapToProc
