I'm doing the lessons on The Odin Project and now I have to write myself a new #count
method (with another name) that behaves like the normal one from the Enumerable module.
The documentation on count says the following (http://ruby-doc.org/core-2.4.0/Enumerable.html#method-i-count):
count → int
count(item) → int
count { |obj| block } → intReturns the number of items in
enum
through enumeration. If an argument is given, the number of items inenum
that are equal toitem
are counted. If a block is given, it counts the number of elements yielding a true value.
I think I can write all of these as separate methods, but I was mostly wondering if one method definition can combine the last two uses of count
- with item
and with the block. Naturally, I'm wondering if all three can be combined in one definition, but I'm mostly interested in the last two. So far I can't seem to find a possible answer.
The documentation page has these examples:
ary = [1, 2, 4, 2]
ary.count #=> 4
ary.count(2) #=> 2
ary.count{ |x| x%2==0 } #=> 3
We can explicitly accept a block in a method by adding it as an argument using an ampersand parameter (usually called &block ). Since the block is now explicit, we can use the #call method directly on the resulting object instead of relying on yield .
In ruby, arguments inside a method are passed by reference In ruby, we have a different situation, the variable that we have inside the method stores a reference to an object. Thus, if we will change an object inside the method, then it will be changed also outside the method.
Ruby has support for methods that accept any number of arguments, either positional or keyword. def some_method(*args) can be called with zero or more parameters. The args variable within the method will be an array of all values passed in when the method is called.
A block is the same thing as a method, but it does not belong to an object. Blocks are called closures in other programming languages.
Sure it's possible. All you have to do is check if an argument is given and also check if a block is given.
def call_me(arg=nil)
puts "arg given" unless arg.nil?
puts "block given" if block_given?
end
call_me(1)
# => arg given
call_me { "foo" }
# => block given
call_me(1) { "foo" }
# => arg given
# block given
Or:
def call_me(arg=nil, &block)
puts "arg given" unless arg.nil?
puts "block given" unless block.nil?
end
The latter is useful because it converts the block to a Proc (named block
) that you can then reuse, as below.
You could implement your own count
method like this:
module Enumerable
def my_count(*args, &block)
return size if args.empty? && block.nil?
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" if args.size > 1
counter = block.nil? ? ->(i) { i == args[0] } : block
sum {|i| counter.call(i) ? 1 : 0 }
end
end
arr = [1,2,3,4,5]
p arr.my_count # => 5
p arr.my_count(2) # => 1
p arr.my_count(&:even?) # => 2
p arr.my_count(2, 3) # => ArgumentError: wrong number of arguments (given 2, expected 1)
See it on repl.it: https://repl.it/@jrunning/YellowishPricklyPenguin-1
Yes, it is possible to do this by making the parameters optional (blocks are always optional anyway) and checking whether a positional argument or a block argument was passed.
This is a bit messy, though. Most Ruby implementations get around this, by implementing the methods in question with privileged access to the private internals of the implementation, which makes it much easier to check whether arguments were passed or not. E.g. both JRuby and IronRuby have ways to bind multiple overloaded Java / CLI methods to a single Ruby method based on the number and the types of arguments, which makes it possible to implement those three "modes" of count
as three simple overloads of the same method. Here's the example of count
from IronRuby, and this is count
from JRuby.
Ruby, however, doesn't support overloading, so you have to implement it manually, which can be a bit awkward. Something like this:
module Enumerable
def count(item = (item_not_given = true; nil))
item_given = !item_not_given
warn 'given block not used' if block_given? && item_given
return count(&item.method(:==)) if item_given
return inject(0) {|acc, el| if yield el then acc + 1 else acc end } if block_given?
count(&:itself)
end
end
As you can see, it is a bit awkward. Why don't I simply use nil
as a default argument for the optional item
parameter? Well, because nil
is a valid argument, and I wouldn't be able to distinguish between someone passing no argument and someone passing nil
as an argument.
For comparison, here is how count
is implemented in Rubinius:
def count(item = undefined) seq = 0 if !undefined.equal?(item) each do element = Rubinius.single_block_arg seq += 1 if item == element end elsif block_given? each { |element| seq += 1 if yield(element) } else each { seq += 1 } end seq end
Where I (ab)use the fact that the default argument for an optional parameter is an arbitrary Ruby expression with side-effects such as setting variables, Rubinius uses a special undefined
object that is provided by the Rubinius runtime and is equal?
only to itself.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With