N Altun N Altun - 1 month ago 8
Ruby Question

Why does Range#select produce an Array object?

Consider I have a Range object,

(1..30).class # => Range


Now consider I am trying to find the factors of
num
,

num = 30
factors = (1..num).select { |n| num % n == 0 }
factors.class # => Array


For
Ruby 2.3.1
a Range object does not have
#select
, but an Array object does. How is calling
Range#select
producing an Array object?

I believe that I am not fully understanding the Ruby Object Model. My current understanding is that
factors.class.eql? Range
should return
true
, not
false
.

factors.class.eql? Array # => true

Answer

The object model in Ruby is simple, single inheritance but with ability to "mixin" modules to add shared behavior. In your case you are using the select method which exists in the module Enumerable. This module is mixed into Array, Hash, and Range. This gives instances of those classes methods such as select. You can read more about enumerable methods here: https://ruby-doc.org/core-2.2.3/Enumerable.html#method-i-select

If you think about it, it makes sense that Range#select returns an Array. You're not selecting contiguous values from the range are you? You're selecting arbitrary values from which the block returns true, this makes it impossible to return a range therefore, #select will always return an array even if it's called on a Hash or any other Class that mixes in Enumerable.

Update:

To understand how Enumerable is returning an Array from a Range

To implement any classes that mix in Enumerable you only have to define the #each method on your class. Say you hypothetically re-implemented Range:

class Range
  include Enumerable # mixin

  def initialize(first, last)
    @number_range = first.upto last # this is an array of ints
  end

  def each(&block) # this methods returns an enumerable
    @number_range.each &block
  end
end

With the above we can initialize our hypothetical range instance:

@hypo_range = Range.new 1, 10

And call enumerable methods on it:

@hypo_range.any? { |i| i == 5 } # => true
@hypo_range.select &:odd? # => [1,3,5,7,9]

Because you need only implement #each to hook into the Enumerable API, Ruby knows exactly what to do with it no matter what the class of the object is. This is because in your new #each method you are iterating over an array already! Enumerable uses your each method under the hood to implement all the other enumerable methods on top e.g. any?, select, find, etc.

That #each method is where you tell Ruby how to iterate over your collection of objects. Once Ruby knows how to iterate over your objects the results are already an Array.

Rubinius implementation of Range

You can see here that Range is implemented by using while to loop from the first value until it reaches the last value and yielding to the block on each iteration. The block collects the results into an Array and that's how you get the Array out of calling Range#select because select is using that each under the hood.

https://github.com/rubinius/rubinius/blob/master/core/range.rb#L118

Some resources:

Comments