可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
Foo.constants
returns an array of module constants as symbols.
method(:const_get)
uses Object#method to perform a method lookup and return a closure.
Foo.method(:const_get).call :Bar
is a closure that returns a qualified path to a constant within the class.
&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?
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 grep
ped.
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:
- Calls
:+.to_proc
and gets a proc object back => #<Proc:0x007fea74028238>
- 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.