Dr Nic

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

Related posts:

  1. map_by_method now works with ActiveRecord associations I was always annoyed that map_by_method was broken for ActiveRecord...
  2. Magic Wiggly Lines => GuessMethod, by Chris Shea If you ever make time to code just for pleasure,...
  3. Magic Multi-Connections: A “facility in Rails to talk to more than one database at a time” At this point in time there’s no facility in Rails...
  4. map_by_method now increasingly more niftier-er. Recap on map_by_method magic: >> p.account.transfers.map_amount # amount method mapped...
  5. My .irbrc for console/irb The relatively unspoken warhorse of Ruby/Rails programming is the irb/console...

17 Responses to “New magical version of Symbol.to_proc”

  1. Dr Nic says:

    Note: the Symbol.to_proc is currently in the Rails libraries (active_support gem) but will be added into Ruby 1.9 one day in the future.

  2. [...] 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. [...]

  3. [...] See original and demo articles. Sex on a stick – soon to be gemified before your very eyes. (Download instructions for the prebuilt gem) [...]

  4. [...] See the original and demo articles. [...]

  5. Piggybox says:

    Dr. Nic,

    I find a bug when using this nice piece of code in Rails console. The singularize part in your code conflicts with the default Rails inflector. This will cause errors when retrieving data for 1-to-many relationship.

    So I just comment out that part of code and use Rails’ own singularize method. Seems fine except another small bug: map_by_method only works on the result array of find, not the array got from 1-to-many convetion like ‘department.people’. I guess the method_missing method is overloaded in such case.

  6. Dr Nic says:

    @piggybox – I discovered this too and instead of solving it :) discovered if I converted the association proxy to an array with .to_a then it worked.

    So, person.friends.map_name would fail, but person.friends.to_a.map_name works.

    One day I might look at the AssocProxy class and see if I can get map_by_method working for it too.

  7. [...] 1.目标: 把 Map by Method(曾经叫做 Map by Pluralisation) library 打包成gem。 查看我们将要打包的代码的相关链接:original and demo。 [...]

  8. Jim Weirich says:

    Hi Dr. Nic. A friend pointed me toward your blog and I’m loving your meta-magic marvels. I hope you don’t mind a question on this older post.

    You embed you method_missing definition inside a module_eval. It seems to me to be sufficient to avoid the included hook and the module_eval altogether and just define method_missing directly in the module. Not only is it simpler, it plays better with the including class if it also defines a method_missing. Is there a reason for included/module_eval that I’m not seeing?

    Thanks … keep those magic moments coming.

  9. Dr Nic says:

    Currently:

    module MapByMethod
      def self.included(base)
        super
    
        base.module_eval < <-EOS
          def method_missing(method, *args, &block)
    ...
    

    if I remove the included/mod_eval:

    module MapByMethod
      def method_missing(method, *args, &block)
    ...
    

    the tests stop working. I think its because ActiveRecord::Base defines #method_missing, so the module's version is not first in the chain.

    Perhaps aliasing might be an alternative?

  10. macovsky says:

    hi there, Nic, sorry for such an old question :)

    could you explain that behaviour of ‘map_by_method’ with activerecord?

    Product.find(:all).collect_save — works fine

    Brand.find(:first).products.collect_save — got NoMethodError

    but Brand.find(:first).products.collect &:save — is ok

  11. Dr Nic says:

    @macovsky [via] – No problems. brand.products doesn’t return an Array, rather an AssociationProxy. Currently map_by_method only works on Array, and I haven’t figured out how to make it work on the AP.

    I tried once, it didn’t work, so I moved on :P Guess I should try again; its annoying that it doesn’t work the same way.

  12. Bug says:

    Yo, sorry to dig this out of its grave, but I can’t help interjecting on your ‘non-fix’ to the problem that Piggybox brought up. All you would have to do to fix that silly problem is change the last line of your mini-library to “Enumerable.send :include, GenericMapToProc”. The results of Thing#find_all or one-to-many attibutes aren’t Arrays, but they are Enumerable.

    Pretty clever stuff, nonetheless.

  13. [...] like customers.collect { |customer| customer.name } in one of his posts. Dr. Nic also has a post talking about the same thing. It’s very interesting to see so many people want to get away [...]

  14. Collect Syntax Study…

    Stephen Chu talked about how to beautify statements like customers.collect { |customer| customer.name…

  15. Nikos D. says:

    Tiny (but maybe important) Rubinius tidbits…

    As you play with a tool you progressively discover various small tidbits about it. I have a few in mind and I thought it would helpful to share them in case you encounter them and get this weird expression on your face of “huh?”.
    1. The multiple ass…

  16. levitra says:

    buy levitra online…

    Stop suffering and buy levitra without prescription, attached is a link….