Search code examples
arraysrubyenumerator

In Ruby, what - technically - does array.reject or array.select without a block do?


So far as I can tell, array.reject and array.select do nothing:

[nil, false, true].reject  # Should have been reject.to_a for this example.
 => [nil, false, true] 
[nil, false, true].select  # Should have been select.to_a for this example.
 => [nil, false, true] 

For the code I was trying to write, compact was what I needed, but I'm very curious why reject and select without a block do nothing - I was expecting a default block of { |e| e } so reject would be compact and 'select' would be some weird anti-compact.

What is the default block doing?


Edit: Sorry, I missed off the '.to_a' on the ends of the expressions above, which I was hoping would trigger some sort of lazy evaluation and make the reject/select enumeration do something useful. I normally cut&paste my examples to avoid this sort of thing.


Solution

  • A block is optional for many Ruby methods. When no block is given an enumerator is usually returned. There are at least a couple of reasons you might want an enumerator.

    #1 Use the enumerator with the methods in the class Enumerator.

    Here's an example. Suppose you wish to alternate the case of letters in a string. One conventional way is:

    "oh happy day".each_char.with_index.map { |c,i| i.odd? ? c.upcase : c.downcase }.join
      #=> "oH HaPpY DaY" 
    

    but you could instead write:

    enum = [:odd, :even].cycle
    "oh happy day".each_char.map { |c| enum.next==:odd ? c.upcase : c.downcase }.join
    

    or perhaps

    enum = [:upcase, :downcase].cycle
    "oh happy day".each_char.map { |c| c.send(enum.next) }.join
    

    See the docs for Array#cycle and Enumerator#next.

    #2 Use enumerators to chain methods

    In my first example above, I wrote:

    "oh happy day".each_char.with_index.map...
    

    If you examine the docs for String#each_char and Enumerator#with_index you will see that both methods can be used with or without a block. Here they are both used without a block. That enables the three methods to be chained.

    Study the return values in the following.

    enum0 = "oh happy day".each_char
      #=> #<Enumerator: "oh happy day":each_char> 
    enum1 = enum0.with_index
      #=> #<Enumerator: #<Enumerator: "oh happy day":each_char>:with_index> 
    enum2 = enum1.map
      #=> #<Enumerator: #<Enumerator: #<Enumerator:
      #     "oh happy day":each_char>:with_index>:map> 
    

    You might want to think of enum1 and enum2 as "compound" enumerators.

    You show the return value of:

    [nil, false, true].reject
    

    to be:

    #=> [nil, false, true]
    

    but that is not correct. The return value is:

    #<Enumerator: [nil, false, true]:reject>
    

    If we write:

    enum = [nil, false, true].reject
    

    then:

    enum.each { |e| e }
      #=> [nil, false] 
    

    (which, since Ruby v2.3, we could write enum.reject(&:itself)). This uses the method Enumerator#each, causing enum to invoke Array#each because reject's receiver is an instance of the class Array.