Enumerable#every

By now rubists know the Symbol#to_proc idiom pretty well

  [1.4, 2.4, 3.4].map {|n| n.floor } #=> [1, 2, 3]
  [1.4, 2.4, 3.4].map(&:floor)       #=> [1, 2, 3]

It has become so popular, in fact, that it’s been included in ruby 1.8.7

Funny thing is, the block form is only an extra 3 characters (6 if you want to count spaces). Though I guess the additional “effort” needed to stop and think about what to name the variable is somewhat distracting, especially since the variable is redundant anyway. Regardless, it’s enough of an improvement on such a commonly used idiom that we happily adopted it.

But after having used it plenty (some rails ./script/console hacking sessions can make quite intense use of it), its syntax started feeling heavy.

My favorite way to access an object’s internals is by far the good ol’ dot. I sometimes use OpenStruct instead of a hash just so I can access the values as methods, and I use `andand.method` instead of `try(:method)` for the same reason.

I knew I would be happier if I could use the same trick with #map, so I wrote an Enumerable method. Compare,

  enum = [1.4, 2.4 ,3.4]
  enum.map {|n| n.floor } #=> [1, 2, 3]
  enum.map(&:floor)       #=> [1, 2, 3]
  enum.every.floor        #=> [1, 2, 3]

It has the added advantage that it can take arguments and blocks, so it’s not restricted to only a few methods.

  %w( axb dxf ).every.gsub(/x/,'y')
  %w( axb dxf ).every.gsub(/x/) { 'y' }

I turned it into a gem so I could easily use it in irb and the rails console, or add it to my rails projects.

To use it:

  # on the command line
  gem sources --add http://gems.github.com
  gem install mynyml-every

  # in your app
  require 'rubygems'
  require 'every'

If you want to use #every as part of a gem or plugin but don’t want to add a dependency for only a few uses, you can simply make it a git submodule, or even just copy the content into your app since it’s contained in a single file.

Project page: http://github.com/mynyml/every

Tags: , , , ,

13 Responses to “Enumerable#every”

  1. Luca Guidi Says:

    Pretty interesting. Did you tried to submit a patch for inclusion in ActiveSupport?

  2. Ruby Enumerable#every « Ruby and Rails Says:

    [...] Martin Aumont a créé une Gem permettant d’itérer et exécuter des actions sur un enumerable encore plus facilement. Voir son article (en anglais). [...]

  3. hgs Says:

    I like this, though I’ve not got into the &:thingy idiom much, myself. I see this change in notation as being significant. I just wonder about extending this so, expressing a simile as a hash,
    {:map => :every, :all? => :every?, :any? => :some?}
    which would give you boolean results for how the application of the method applies to at least one member of, or all of, the collection.

  4. Eric Anderson Says:

    That is friggen AWESOME.

    On one hand I have always felt that Symbol#to_proc was nicer than using a full block. But on the other hand arr.map(&:strip) always felt like too much line noise to be Ruby. Felt like I was moving back to Perl. The lack of arguments and blocks also was annoying as you gsub example demonstrates.

    But your solution is makes it work better AND more elegant. I will be using this on many projects/tasks to come.

  5. citizen428.blog() Says:

    Enumberable#filter…

    As much as I like Ruby, some constructs I just don’t find too sexy, e.g.:

    [1,2,5,8,7,3,1,9,5].select { |x| x < 5 }

    That’s why I came up with alternate solution which I find reads nicer than the above code:

    I kinda got inspire…

  6. mynyml Says:

    @Luca yes, I’m working on an ActiveSupport patch atm, but it’ll probably be another 2-3 days before I submit it.

    @hgs I’m tempted to add other methods too, but at the same time I want to keep the gem as simple as possible. Currently you can have the same results with:

    enum = [ "a", "b", "" ]
    enum.every.empty?.all? #=> false
    enum.every.empty?.any? #=> true

    Though I must admit #every? and #some? are sexier names! I’ll definetly keep those in mind. Thanks for the suggestion.

    Btw there’s an expiremental branch in the repo that adds the idiom for #select and #reject (@ http://github.com/mynyml/every/tree/expanded)

    @Eric glad you liked it!

  7. Shadowfiend Says:

    Very sexy. A variant on Symbol#to_proc that allows parameters and multiple method calls is the Methodphetamine (http://jicksta.com/posts/the-methodphitamine). It still uses the & syntax, but without many of the limitations of the more traditional #to_proc solution.

  8. mynyml Says:

    @Shadowfiend Interesting. I didn’t know about Methodphetamine. Pretty clever. Makes for an interesting comparison, actually;

    (first three examples from: http://jicksta.com/posts/the-methodphitamine)
    User.all.map{|x| x.contacts.map{|y| y.last_name.capitalize }}
    User.all.map{|x|x.contacts.map(&:last_name).map(&:capitalize)}
    User.all.map &its.contacts.map(&its.last_name.capitalize)
    User.all.every.contacts.flatten.every.last_name.every.capitalize

  9. coderrr Says:

    Hobo has had this for a long time. They use *

    User.all.*.contacts.flatten.*.last_name.*.capitalize

    http://github.com/tablatom/hobo/blob/d8b5b90d630e3911fab8655275cc043217bc32d8/hobosupport/lib/hobo_support/enumerable.rb

  10. Ryan Davis Says:

    # of iterations = 1000000
    user system total real
    null_time 0.260000 0.000000 0.260000 ( 0.281800)
    map 17.120000 0.130000 17.250000 ( 19.205743)
    to_proc 44.220000 0.430000 44.650000 ( 50.080658)
    every 66.930000 0.670000 67.600000 ( 76.222073)

  11. mynyml Says:

    There are benchmarks included in the gem that show the same results. There’s no doubt that #every is slower than native #map, and it is slower than Symbol#to_proc.

    On ruby v1.8.7 (100_000 iterations)

                        total        real
    #map:            0.170000 (  0.202195)
    Symbol#to_proc:  0.150000 (  0.144365)
    #every:          0.240000 (  0.251681)
    

    IMO the difference is not significant enough to matter, though. Noone’s expected to use it in a low level library (where you’d probably use native methods as much as possible, or even better, RubyInline ;) ). And 100_000 iterations (or 1_000_000 in your benchmark) is a *lot* of iterations; it’s unlikely your biggest worry will be #every.

  12. slothbear Says:

    Very very nice. Makes it look like .. no … *makes* it real Ruby.

  13. Nicolas Blanco Says:

    I really like this.
    Should be in Rails core for sure!

    Nicolas.