Может ли метод 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/ (который напомнил мне, что метод может выдавать блок, даже если он не определен как аргумент, такой как и блок). Я видел, что Йорг также прокомментировал обсуждение первой ссылки.