Подтвердить что ты не робот

Как работает идиома "#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]
      

Вопрос, пересмотренный

Мне бы очень хотелось понять эту идиому в целом. Может ли кто-нибудь заполнить пробелы здесь и объяснить, как все элементы подходят друг к другу?

4b9b3361

Ответ 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

и вам, вероятно, они не нужны.