Dr Nic

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

Code available here

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

Ruby Forum

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