instance_eval's block argument(s)- documented? purpose?

前端 未结 3 1772
悲&欢浪女
悲&欢浪女 2021-01-12 11:28

Just realized that instance_eval yields self as an argument to the associated block (except for a bug in the 1.9.2 version: http://www.ruby-forum.c

相关标签:
3条回答
  • 2021-01-12 11:47

    I just asked the same question here: Ruby lambda's proc's and 'instance_eval'

    And after reading the answer and working through some code, I think I understand why ruby has this strange (IMHO) inconsistency.

    It basically allows Symbol#to_proc to work.

    For example ["foo", "bar"].each(&:puts) is short for [...].each { |x| puts x }

    NOT

    [...].each { self.puts }

    So ruby also passes self as the first param to the proc, so basically the proc can either use self or its first param.

    Since instance eval does not by definition explicitly pass params this is almost always invisible behavior.

    The exception is when the proc is a lambda. This DOES NOT WORK:

    2.4.1 :015 > foo = -> { puts 'hi' }
     => #<Proc:0x007fcb578ece78@(irb):15 (lambda)> 
    2.4.1 :016 > [1, 2, 3].each(&foo)
    ArgumentError: wrong number of arguments (given 1, expected 0)
        from (irb):15:in `block in irb_binding'
        from (irb):16:in `each'
        from (irb):16
    

    So I think the only time this becomes a problem is when instance_eval is being used with some unknown value, where you don't know if the proc is a lambda or not. In this case you have to do it like this:

    proc_var.lambda? ? instance_exec(&proc_var) : instance_eval(&proc_var)

    Weird (to me) that ruby just does not do this under the hood for you.

    but I guess you could make it so:

    alias original_instance_eval instance_eval 
    def instance_eval(*args, &block)
      block&.lambda? ? instance_exec(&block) : original_instance_eval(*args, &block)
    end
    
    0 讨论(0)
  • 2021-01-12 11:58

    Reading Ruby's source code, what I can interpret is:

    instance_eval is executing this:

    return specific_eval(argc, argv, klass, self)
    

    which in turn runs:

     if (rb_block_given_p()) {
         if (argc > 0) {
             rb_raise(rb_eArgError, "wrong number of arguments (%d for 0)", argc);
         }
         return yield_under(klass, self, Qundef);
     }
    

    You can see they pass Qundef for the VALUES argument.

    if (values == Qundef) {
        return vm_yield_with_cref(th, 1, &self, cref);
    }
    

    In that particular line of code, they set manually argc (argument count) to 1 and the argument as "self". Later on the code that prepares the block sets the arguments to the block to these arguments, hence the first argument = "self" and the rest are nil.

    The code that sets up the block arguments is doing :

       arg0 = argv[0];
    
       ... bunch of code ...
    
         else {
             argv[0] = arg0;
         }
    
         for (i=argc; i<m; i++) {
             argv[i] = Qnil;
         }
    

    Resulting in:

    1.9.3p194 :006 > instance_eval do |x, y, z, a, b, c, d| x.class end
     => Object 
    1.9.3p194 :008 > instance_eval do |x, y, z, a, b, c, d| y.class end
     => NilClass 
    

    Why ? I have no idea but the code seems to be intentional. Would be nice to ask the question to the implementers and see what they have to say about it.

    [Edit]

    This probably is like that because the blocks you pass to instance_eval may or may not be crafted for it (code that depends on self being set to the class you want the block to modify), instead they may assume you are going to pass them the instance you want them to modify as an argument and in this way they would work with instance_eval as well.

    irb(main):001:0> blk = Proc.new do |x| x.class end
    #<Proc:0x007fd2018447b8@(irb):1>
    irb(main):002:0> blk.call
    NilClass
    irb(main):003:0> instance_eval &blk
    Object
    

    Of course this is only a theory and without official documentation I can only guess.

    0 讨论(0)
  • 2021-01-12 12:02

    I have just dicovered that unlike #instance_eval, which is primarily intended for string evaluation, #instance_exec primarily intended for block evaluation, does not have the described behavior:

    o = Object.new
    o.instance_exec { |*a| puts "a.size is #{a.size}" }
      => a.size is 0
    

    This is probably an unintended inconsistency, so you might have discovered a bug. Post it on Ruby bugs.

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