John Quarles
John Quarles

Reputation: 21

How does Ruby's #count method deal with nil values?

I'm doing Ruby exercises for the Odin Project (programming newcomer), and we're tasked with recreating Ruby's #count method. Given an array like:

nil_list = [false, false, nil]

Observations:

When I try to recreate it, here's what I come up with:

module Enumerable
  def my_count (find = nil)
    result = 0
    for i in self
      if block_given?
        result += 1 if yield(i)
      elsif find != nil
        result += 1 if i == find
      else return self.length
      end
    end
    return result
  end
end

The problem here is that this doesn't actually count nils if we enter nil in as an argument, since this is the same (according to my code) as there not being an argument.

ie, nil_list.my_count(nil) == 3 instead of 1.

While typing this question I had a slightly different idea:

module Enumerable
  def my_count (find = "")
    result = 0
    for i in self
      if block_given?
        result += 1 if yield(i)
      elsif find != ""
        result += 1 if i == find
      else return self.length
      end
    end
    return result
  end
end

So this fixes the problem I was having with searches for nil, but now nil_list.count("") == 0 whereas nil_list.my_count("") == 3. Same issue, just relocated to "" which I assume doesn't ever get used.

At this point I'm just curious: how does the actual count method prevent this issue from happening?

Upvotes: 2

Views: 2055

Answers (2)

Jörg W Mittag
Jörg W Mittag

Reputation: 369468

The ugly truth is: in most Ruby implementations, Enumerable#count isn't actually written in Ruby. In MRI, YARV and MRuby, it's written in C, in JRuby and XRuby, it's written in Java, in IronRuby and Ruby.NET, it's written in C#, in MacRuby, it's written in Objective-C, in MagLev, it's written in Smalltalk, in Topaz, it's written in RPython, in Cardinal, it's written in PIR or PASM, and so on. And it not only is not written in Ruby, it's also got privileged access to the internals of the execution engine, in particular, it can access the arguments that were passed, which you cannot do from Ruby.

Such overloaded methods appear all over the core library and standard library, but they can't easily be written in Ruby. The implementers cheat by either writing them in languages that do support overloading (e.g. C# or Java), or they give them privileged access to the internals of the execution engine.

The standard workaround in Ruby is to (ab)use the fact that the default value of an optional parameter is just a normal Ruby expression and that local variables in a default value expression are visible inside the method body:

def my_count(find = (find_passed = false; nil))
  if find_passed # find was passed
    # do something
  else
    # do something else
  end
end

A second possibility is to use some unforgeable unique token as the default value:

undefined = Object.new
define_method(:my_count) do |find = undefined|
  if undefined.equal?(find) # find was not passed
    # do something
  else
    # do something else
  end
end

Upvotes: 1

tokland
tokland

Reputation: 67870

You can write def my_count(*args) and check then length of args. I'd write:

module Enumerable
  def my_count(*args)
    case
    when args.size > 1
      raise ArgumentError
    when args.size == 1
      value = args.first
      reduce(0) { |acc, x| value == x ? acc + 1 : acc }
    when block_given?
      reduce(0) { |acc, x| yield(x) ? acc + 1 : acc }
    else
      reduce(0) { |acc, x| acc + 1 }
    end
  end
end

Upvotes: 2

Related Questions