How does the “#map(&proc)” idiom work when introspecting module classes?

前端 未结 4 1532
长情又很酷
长情又很酷 2021-02-13 13:29

Presenting the Idiom

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

module          


        
相关标签:
4条回答
  • 2021-02-13 13:59

    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.

    0 讨论(0)
  • 2021-02-13 14:00

    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.

    0 讨论(0)
  • 2021-02-13 14:14

    &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.

    0 讨论(0)
  • 2021-02-13 14:20

    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.

    0 讨论(0)
提交回复
热议问题