1

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

N Altun
  • 95
  • 1
  • 2
  • 9
  • The first two lines of your code works (returning `[1, 2, 3, 5, 6, 10, 15, 30]`). That means that that object `1..num`, a range, has a method `select`. You can confirm that (though there really is no need): `(1..num).methods.include?(:select) #=> true`. Where is `select` defined? There are three possibilities: in the current class, in an ancestor of that class or in an included module. We need to find the method's *owner*. First create a method object for `select`: `m = (1..num).method(:select) #=> #`. Then obtain its owner: `m.owner #=> Enumerable`... – Cary Swoveland Oct 29 '16 at 03:07
  • ... We also see `Range.included_modules #=> [Enumerable, Kernel] `. So here `select` is from the included module `Enumerable`. Have a look (in my first comment) at the return value of the expression that computes `m`. You'll see that it contains `"(Enumerable)"`, which is the owner. Therefore we actually didn't have to execute `m.owner` to find `select`'s owner. The docs for the methods I've used are at [Object#method](http://ruby-doc.org/core-2.3.0/Object.html#method-i-method) and [Method#owner](http://ruby-doc.org/core-2.3.0/Method.html#method-i-owner). – Cary Swoveland Oct 29 '16 at 03:15

2 Answers2

2

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:

DiegoSalazar
  • 13,361
  • 2
  • 38
  • 55
  • Okay, so that initially makes sense. `#select` is returning a list not conforming to a `Range` object. But How does the Ruby Object Model dictate that the evaluation of `factors` IS in fact an Array if it started off as a Range? This was a perfect example to begin understanding `mixins` nicely. But I'm not sure how Ruby doesn't break because I am still working on a Range object. So my question: How does using `Range#select` return an Array if Enumerable methods are mixed in/moduled into Range? To me it seems like it'd still return a Range. @Ursus feel free to comment. – N Altun Oct 28 '16 at 18:34
  • Brilliant! Thanks @diego.greyrobot. I love to use Ruby, but it's very intricate. I suppose that's why Matz said something along the lines of, 'Ruby appears simple, but is rather complex, just like our human bodies.' – N Altun Oct 28 '16 at 18:55
  • I find it quite cuddly as well :) – DiegoSalazar Oct 28 '16 at 18:55
  • It should be noted that, while the `Enumerable` module is included in the classes `Array`, `Hash` and `Range` (and others), [Enumerable#select](http://ruby-doc.org/core-2.3.0/Enumerable.html#method-i-select) (which returns an array, or an enumerator) is not executed on arrays or hashes... – Cary Swoveland Oct 28 '16 at 22:18
  • ...In the case of arrays, [Array#select](http://ruby-doc.org/core-2.3.0/Array.html#method-i-select) (which returns the same array as `Enumerable#select`) is executed. It is provided because it is more efficient than the more general `Enumerable#select`. For hashes, [Hash#select](http://ruby-doc.org/core-2.3.0/Hash.html#method-i-select), which returns a hash, rather than an array, is executed. – Cary Swoveland Oct 28 '16 at 22:18
  • Thank you @CarySwoveland – DiegoSalazar Oct 28 '16 at 22:20
1

Check the docs for Range http://ruby-doc.org/core-2.3.1/Range.html

It says included modules Enumerable. And that's where there are implemented map, all?, any?, find, select, inject and many many more methods.

Ursus
  • 29,643
  • 3
  • 33
  • 50