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: enumerable, every, map, ruby, Symbol#to_proc
April 1st, 2009 at 09:36
Pretty interesting. Did you tried to submit a patch for inclusion in ActiveSupport?
April 1st, 2009 at 09:55
[...] 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). [...]
April 1st, 2009 at 12:20
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.
April 1st, 2009 at 14:01
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.
April 1st, 2009 at 16:37
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…
April 1st, 2009 at 18:28
@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!
April 2nd, 2009 at 00:25
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.
April 2nd, 2009 at 00:47
@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
April 2nd, 2009 at 10:31
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
April 2nd, 2009 at 23:21
# 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)
April 3rd, 2009 at 10:17
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.
April 6th, 2009 at 22:29
Very very nice. Makes it look like .. no … *makes* it real Ruby.
April 14th, 2009 at 08:19
I really like this.
Should be in Rails core for sure!
Nicolas.