Может ли метод Ruby принять блок ИЛИ аргумент?

Я делаю уроки по проекту Odin, и теперь я должен написать себе новый #count метод (с другим именем), который ведет себя как обычный из модуля Enumerable.

Документация по подсчетам гласит следующее ( http://ruby-doc.org/core-2.4.0/Enumerable.html):

считать → int
count (item) → int
count {| obj | блок} → int

Возвращает количество предметов в enum через перечисление. Если указан аргумент, количество элементов в enum которые равны item посчитаны. Если задан блок, он подсчитывает количество элементов, дающих истинное значение.

Я думаю, что могу написать все это как отдельные методы, но мне было интересно, может ли одно определение метода объединить два последних использования count - с item и с блоком. Естественно, мне интересно, могут ли все три быть объединены в одном определении, но меня больше всего интересуют последние два. Пока я не могу найти возможный ответ.

На странице документации есть следующие примеры:

ary = [1, 2, 4, 2]
ary.count               #=> 4
ary.count(2)            #=> 2
ary.count{ |x| x%2==0 } #=> 3

3 ответа

Решение

Конечно, это возможно. Все, что вам нужно сделать, это проверить, задан ли аргумент, а также проверить, задан ли блок.

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

Или же:

def call_me(arg=nil, &block)
  puts "arg given" unless arg.nil?
  puts "block given" unless block.nil?
end

Последний полезен, потому что он преобразует блок в Proc (названный block) что вы можете использовать повторно, как показано ниже.

Вы могли бы реализовать свой собственный count метод как это:

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
    reduce(0) {|cnt,i| counter.call(i) ? cnt + 1 : cnt }
  end
end

p [1,2,3,4,5].my_count # => 5
p [1,2,3,4,5].my_count(2) # => 1
p [1,2,3,4,5].my_count {|i| i % 2 == 0 } # => 2

Смотрите его на repl.it: https://repl.it/FcNs

Да, это можно сделать, сделав параметры необязательными (блоки всегда всегда необязательны) и проверив, был ли передан позиционный аргумент или аргумент блока.

Это немного грязно, хотя. Большинство реализаций Ruby обходят это путем реализации соответствующих методов с привилегированным доступом к закрытым внутренним компонентам реализации, что значительно упрощает проверку того, были ли переданы аргументы или нет. Например, и JRuby, и IronRuby имеют способы привязать несколько перегруженных методов Java / CLI к одному методу Ruby на основе количества и типов аргументов, что позволяет реализовать эти три "режима" count как три простых перегрузки одного и того же метода. Вот пример count от IronRuby, и это count от JRuby.

Ruby, однако, не поддерживает перегрузку, поэтому вы должны реализовать ее вручную, что может быть немного неудобно. Что-то вроде этого:

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

Как видите, это немного неловко. Почему бы мне просто не использовать nil в качестве аргумента по умолчанию для необязательного item параметр? Ну потому что nil является действительным аргументом, и я не смогу отличить того, кто не передает аргументов, и того, кто передает nil в качестве аргумента.

Для сравнения вот как count реализовано в Рубиниусе:

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

Там, где я (ab) использую тот факт, что аргумент по умолчанию для необязательного параметра является произвольным выражением Ruby с побочными эффектами, такими как установка переменных, Rubinius использует специальный undefined объект, который предоставляется средой выполнения Rubinius и является equal? только себе.

Спасибо за помощь! Незадолго до того, как я пришел проверить, есть ли какие-либо ответы, я нашел следующее решение. Это может быть определенно улучшено, и я постараюсь немного сократить его, но я предпочитаю сначала опубликовать это здесь, так как я придумал это, это может быть полезно для других новичков, таких как я. В приведенном ниже коде я использую метод #my_each, который работает так же, как обычный #each.

def my_count(arg=nil)
    sum = 0
    if block_given? && arg == nil
        self.my_each do |elem|
            if yield(elem)
                sum += 1
            end
        end
    elsif !block_given? && arg != nil
        self.my_each do |elem|
            if arg == elem
                sum += 1
            end
        end
    else
        self.my_each do |elem|
            sum += 1
        end
    end
    sum
end

Я также нашел эти две ссылки полезными: метод с необязательным параметром и http://augustl.com/blog/2008/procs_blocks_and_anonymous_functions/ (который напомнил мне, что метод может выдавать блок, даже если он не определен как аргумент, такой как и блок). Я видел, что Йорг также прокомментировал обсуждение первой ссылки.

Другие вопросы по тегам