New magical version of Symbol.to_proc

Posted by Dr Nic on September 28, 2006

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
Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. Dr Nic Thu, 28 Sep 2006 15:18:46 UTC

    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. Dr Nic » My .irbrc for console/irb Thu, 12 Oct 2006 13:06:42 UTC

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

  5. Piggybox Tue, 24 Oct 2006 18:25:51 UTC

    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 Tue, 24 Oct 2006 18:45:41 UTC

    @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. [...] Recap on map_by_method magic: [...]

  8. KDr2 » 简单漂亮的打包GEM Fri, 05 Jan 2007 09:49:25 UTC

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

  9. Jim Weirich Thu, 19 Apr 2007 02:59:52 UTC

    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.

  10. Dr Nic Thu, 19 Apr 2007 13:29:48 UTC

    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?

  11. macovsky Mon, 09 Jul 2007 10:03:42 UTC

    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

  12. Dr Nic Mon, 09 Jul 2007 10:32:07 UTC

    @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.

  13. Bug Sun, 19 Aug 2007 04:43:37 UTC

    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.

  14. Collect Syntax Study « Dynamic and Concise Tue, 04 Dec 2007 18:10:57 UTC

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

  15. Dynamic and Concise Tue, 04 Dec 2007 18:15:39 UTC

    Collect Syntax Study…

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

  16. Nikos D. Sun, 20 Apr 2008 03:11:51 UTC

    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…

Comments