How does the “#map(&proc)” idiom work when introsp

2020-06-09 06:51发布

问题:

Presenting the Idiom

I found an interesting but unexplained alternative to an accepted answer. The code clearly works in the REPL. For example:

module Foo
  class Bar
    def baz
    end
  end
end
Foo.constants.map(&Foo.method(:const_get)).grep(Class)
=> [Foo::Bar]

However, I don't fully understand the idiom in use here. In particular, I don't understand the use of &Foo, which seems to be some sort of closure, or how this specific invocation of #grep operates on the result.

Parsing the Idiom

So far, I've been able to parse bits and pieces of this, but I'm not really seeing how it all fits together. Here's what I think I understand about the sample code.

  1. Foo.constants returns an array of module constants as symbols.

  2. method(:const_get) uses Object#method to perform a method lookup and return a closure.

  3. Foo.method(:const_get).call :Bar is a closure that returns a qualified path to a constant within the class.

  4. &Foo seems to be some sort of special lambda. The docs say:

    The & argument preserves the tricks if a Proc object is given by & argument.

    I'm not sure I fully understand what that means in this specific context, either. Why a Proc? What "tricks," and why are they necessary here?

  5. grep(Class) is operating on the value of the #map method, but its features are not obvious.

    • Why is this #map construct returning a greppable Array instead of an Enumerator?

      Foo.constants.map(&Foo.method(:const_get)).class
      => Array
      
    • How does grepping for a class named Class actually work, and why is that particular construction necessary here?

      [Foo::Bar].grep Class
      => [Foo::Bar]
      

The Question, Restated

I'd really like to understand this idiom in its entirety. Can anyone fill in the gaps here, and explain how the pieces all fit together?

回答1:

&Foo.method(:const_get) is the method const_get of the Foo object. Here's another example:

m = 1.method(:+)
#=> #<Method: Fixnum#+>
m.call(1)
#=> 2
(1..3).map(&m)
#=> [2, 3, 4]

So in the end this is just a pointfree way of saying Foo.constants.map { |c| Foo.const_get(c) }. grep uses === to select elements, so it would only get constants that refer to classes, not other values. This can be verified by adding another constant to Foo, e.g. Baz = 1, which will not get grepped.

If you have further questions please add them as comments and I'll try to clarify them.



回答2:

Your parse of the idiom is pretty spot on, but I'll go through it and try to clear up any questions you mentioned.

1. Foo.constants

As you mentioned, this returns an array of module constant names as symbols.

2. Array#map

You obviously know what this does, but I want to include it for completeness. Map takes a block and calls that block with each element as an argument. It returns an Array of the results of these block calls.

3. Object#method

Also as you mentioned, this does a method lookup. This is important because a method without parentheses in Ruby is a method call of that method without any arguments.

4. &

This operator is for converting things to blocks. We need this because blocks are not first-class objects in Ruby. Because of this second-class status, we have no way to create blocks which stand alone, but we can convert Procs into blocks (but only when we are passing them to a function)! The & operator is our way of doing this conversion. Whenever we want to pass a Proc object as if it were a block, we can prepend it with the & operator and pass it as the last argument to our function. But & can actually convert more than just Proc objects, it can convert anything that has a to_proc method!

In our case, we have a Method object, which does have a to_proc method. The difference between a Proc object and a Method object lies in their context. A Method object is bound to a class instance and has access to the variables which belong to that class. A Proc is bound to the context in which it is created; that is, it has access to the scope in which it was created. Method#to_proc bundles up the context of the method so that the resulting Proc has access to the same variables. You can find more about the & operator here.

5. grep(Class)

The way Enumerable#grep works is that it runs argument === x for all x in the enumerable. The ordering of the arguments to === is very important in this case, since it's calling Class.=== rather than Foo::Bar.===. We can see the difference between these two by running:

    irb(main):043:0> Class === Foo::Bar
    => true
    irb(main):044:0> Foo::Bar === Class
    => false

Module#=== (Class inherits its === method from Method) returns True when the argument is an instance of Module or one of its descendants (like Class!), which will filter out constants which are not of type Module or Class. You can find the documentation for Module#=== here.



回答3:

The first thing to know is that:

& calls to_proc on the object succeeding it and uses the proc produced as the methods' block.

Now you have to drill down to how exactly the to_proc method is implemented in a specific class.

1. Symbol

class Symbol
  def to_proc
    Proc.new do |obj, *args|
      obj.send self, *args
    end
  end
end

Or something like this. From the above code you clearly see that the proc produced calls the method (with name == the symbol) on the object and passes the arguments to the method. For a quick example:

[1,2,3].reduce(&:+)
#=> 6

which does exactly that. It executes like this:

  1. Calls :+.to_proc and gets a proc object back => #<Proc:0x007fea74028238>
  2. It takes the proc and passes it as the block to the reduce method, thus instead of calling [1,2,3].reduce { |el1, el2| el1 + el2 } it calls
    [1,2,3].reduce { |el1, el2| el1.send(:+, el2) }.

2. Method

 class Method
   def to_proc
     Proc.new do |*args|
       self.call(*args)
     end
   end
 end

Which as you can see it has a different implementation of Symbol#to_proc. To illustrate this consider again the reduce example, but now let as see how it uses a method instead:

def add(x, y); x + y end
my_proc = method(:add)
[1,2,3].reduce(&my_proc)
#=> 6

In the above example is calling [1,2,3].reduce { |el1, el2| my_proc(el1, el2) }.

Now on why the map method returns an Array instead of an Enumerator is because you are passing it a block, try this instead:

[1,2,3].map.class
#=> Enumerator

Last but not least the grep on an Array is selecting the elements that are === to its argument. Hope this clarifies your concerns.



回答4:

Your sequence is equivalent to:

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]

You can consider Object#method as (roughly):

class Object
  def method(m)
    lambda{ |*args| self.__send__(m, *args) }
  end
end

grep is described here http://ruby-doc.org/core-1.9.3/Enumerable.html#method-i-grep

=== for Class (which is subclass of Module) is described here http://ruby-doc.org/core-1.9.3/Module.html#method-i-3D-3D-3D

UPDATE: And you need to grep because there can be other constants:

module Foo
  PI = 3.14
  ...
end

and you probably don't need them.