Как работает идиома "#map (& proc)" при изучении классов модулей?
Представление Idiom
Я нашел интересную, но необъяснимую альтернативу принятому ответу. Код явно работает в REPL. Например:
module Foo
class Bar
def baz
end
end
end
Foo.constants.map(&Foo.method(:const_get)).grep(Class)
=> [Foo::Bar]
Однако я не полностью понимаю используемую здесь идиому. В частности, я не понимаю использование &Foo
, которое, похоже, является своего рода закрытием или как этот конкретный вызов #grep работает на результат.
Анализ идиомы
До сих пор я мог разбирать кусочки этого, но я не вижу, как все это сочетается. Вот что я думаю, что я понимаю пример кода.
-
Foo.constants
возвращает массив констант модуля в качестве символов.
-
method(:const_get)
использует Object # method для выполнения поиска метода и возврата замыкания.
-
Foo.method(:const_get).call :Bar
- это замыкание, которое возвращает квалифицированный путь к константе внутри класса.
-
&Foo
кажется своего рода специальная лямбда. Документы говорят:
Аргумент и аргумент сохраняют трюки, если объект Proc задается аргументом и аргументом.
Я не уверен, что полностью понимаю, что это значит в этом конкретном контексте. Почему Proc? Какие "трюки" и зачем они нужны здесь?
-
grep(Class)
работает над значением # map, но его функции не очевидны.
-
Почему эта конструкция #map возвращает greppable Array вместо Enumerator?
Foo.constants.map(&Foo.method(:const_get)).class
=> Array
-
Как работает grepping для класса с именем Class, и зачем нужна эта конкретная конструкция здесь?
[Foo::Bar].grep Class
=> [Foo::Bar]
Вопрос, пересмотренный
Мне бы очень хотелось понять эту идиому в целом. Может ли кто-нибудь заполнить пробелы здесь и объяснить, как все элементы подходят друг к другу?
Ответы
Ответ 1
&Foo.method(:const_get)
- это метод const_get
объекта Foo
. Вот еще один пример:
m = 1.method(:+)
#=> #<Method: Fixnum#+>
m.call(1)
#=> 2
(1..3).map(&m)
#=> [2, 3, 4]
Итак, в конце это всего лишь pointfree способ сказать Foo.constants.map { |c| Foo.const_get(c) }
. grep
использует ===
для выбора элементов, поэтому он будет получать только константы, относящиеся к классам, а не другие значения. Это можно проверить, добавив еще одну константу в Foo
, например. Baz = 1
, который не получит grep
ped.
Если у вас есть дополнительные вопросы, добавьте их в качестве комментариев, и я попытаюсь их прояснить.
Ответ 2
Ваш синтаксический анализ идиомы довольно приятный, но я пройду через него и попытаюсь прояснить все ваши вопросы.
1. Foo.constants
Как вы упомянули, это возвращает массив имен констант модуля в виде символов.
2. Array#map
Вы, очевидно, знаете, что это делает, но я хочу включить его для полноты. Карта берет блок и вызывает этот блок с каждым элементом в качестве аргумента. Он возвращает Array
результатов этих блочных вызовов.
3. Object#method
Также, как вы упомянули, это поиск метода. Это важно, потому что метод без круглых скобок в Ruby является вызовом метода этого метода без каких-либо аргументов.
4. &
Этот оператор предназначен для преобразования объектов в блоки. Нам это нужно, потому что блоки не являются первоклассными объектами в Ruby. Из-за этого статуса второго класса у нас нет возможности создавать блоки, которые стоят отдельно, но мы можем преобразовать Procs
в блоки (но только когда мы передаем их функции)! Оператор &
- это наш способ сделать это преобразование. Всякий раз, когда мы хотим передать объект Proc
, как если бы он был блоком, мы можем добавить его с помощью оператора &
и передать его в качестве последнего аргумента нашей функции. Но &
может фактически преобразовать больше, чем просто объекты Proc
, он может конвертировать все, что имеет метод to_proc
!
В нашем случае мы имеем объект Method
, который имеет метод to_proc
. Разница между объектом Proc
и объектом Method
заключается в их контексте. Объект Method
привязан к экземпляру класса и имеет доступ к переменным, принадлежащим этому классу. A Proc
привязан к контексту, в котором он создан; то есть он имеет доступ к области, в которой он был создан. Method#to_proc
связывает контекст метода, так что полученный Proc
имеет доступ к тем же переменным. Подробнее об операторе &
здесь.
5. grep(Class)
Способ Enumerable#grep
заключается в том, что он запускает argument === x
для всех x в перечислимом. В этом случае порядок аргументов ===
очень важен, поскольку он вызывает Class.===
, а не Foo::Bar.===
. Мы можем видеть разницу между этими двумя:
irb(main):043:0> Class === Foo::Bar
=> true
irb(main):044:0> Foo::Bar === Class
=> false
Module#===
(Class
наследует свой метод ===
от Method
) возвращает True
, когда аргумент является экземпляром Module
или одним из его потомков (например, Class
!), который будет отфильтровать константы, которые не относятся к типу Module
или Class
.
Вы можете найти документацию для Module#===
здесь.
Ответ 3
Первое, что нужно знать, это то, что:
&
вызывает to_proc
для объекта, следующего за ним, и использует proc, созданный как блок методов.
Теперь вам нужно перейти к тому, как именно метод to_proc
реализуется в определенном классе.
1. Символ
class Symbol
def to_proc
Proc.new do |obj, *args|
obj.send self, *args
end
end
end
Или что-то вроде этого. Из приведенного выше кода вы ясно видите, что обработанный proc вызывает метод (с именем == символ) объекта и передает аргументы методу. Для быстрого примера:
[1,2,3].reduce(&:+)
#=> 6
что делает именно это. Он выполняется следующим образом:
- Вызывает
:+.to_proc
и возвращает объект proc => #<Proc:0x007fea74028238>
- Он принимает proc и передает его как блок методу
reduce
, поэтому вместо вызова [1,2,3].reduce { |el1, el2| el1 + el2 }
он вызывает
[1,2,3].reduce { |el1, el2| el1.send(:+, el2) }
.
2. Метод
class Method
def to_proc
Proc.new do |*args|
self.call(*args)
end
end
end
Что, как вы видите, имеет другую реализацию Symbol#to_proc
. Чтобы проиллюстрировать это, снова рассмотрим пример reduce
, но теперь давайте посмотрим, как он использует метод:
def add(x, y); x + y end
my_proc = method(:add)
[1,2,3].reduce(&my_proc)
#=> 6
В приведенном выше примере вызывается [1,2,3].reduce { |el1, el2| my_proc(el1, el2) }
.
Теперь почему метод map
возвращает Array вместо Enumerator, потому что вы передаете ему блок, попробуйте это вместо этого:
[1,2,3].map.class
#=> Enumerator
И последнее, но не менее важное: grep
в массиве выбирает в свой аргумент элементы ===
. Надеюсь, это разъяснит ваши проблемы.
Ответ 4
Ваша последовательность эквивалентна:
c_names = Foo.constants #=> ["Bar"]
cs = c_names.map { |c_name| Foo.__send__(:const_get, c_name) } #=> [Foo::Bar]
cs.select{ |c| Class === c } #=> [Foo::Bar]
Вы можете рассматривать Object#method
как (примерно):
class Object
def method(m)
lambda{ |*args| self.__send__(m, *args) }
end
end
grep
описывается здесь http://ruby-doc.org/core-1.9.3/Enumerable.html#method-i-grep
===
для Class
(который является подклассом Module
) описан здесь http://ruby-doc.org/core-1.9.3/Module.html#method-i-3D-3D-3D
ОБНОВЛЕНИЕ. И вам нужно grep
, потому что могут быть другие константы:
module Foo
PI = 3.14
...
end
и вам, вероятно, они не нужны.