4

I'm writing a method that splits an array of struct into a couple of different, arrays while also eliminating elements with nil values.

What I want to write is:

def my_func(some_data)
    f = lambda{|data| data.select{|m| yield(m).present? }.map { |m| [m.date, yield(m)]}}

    x = f.call(some_data) {|m| m.first_var}
    y = f.call(some_data) {|m| m.second_var}

    return x, y
end

This appears not to work as Ruby doesn't handle the yield inside the lambda in the way I expect.

LocalJumpError: no block given(yield)

Is there a way to define f so that x and y give the result I'm looking for?

Ben Fulton
  • 3,988
  • 3
  • 19
  • 35

1 Answers1

6

You cannot yield from a lambda directly to a nonexplicit block passed to it as you are doing now (See comments below for source provided by @cremno showing that yield may only be used in method and singleton class definitions)

Source Extraction:

if ((type != ISEQ_TYPE_METHOD && type != ISEQ_TYPE_CLASS) || block == 0) {
  rb_vm_localjump_error("no block given (yield)", Qnil, 0);
}

Primary Solution:

You could do this with an explicit block like so

f = lambda do |data,&block| 
      data.select do |m| 
        block.call(m)
      end.map do |m| 
        [m, block.call(m)]
      end
    end
d = ["string",1,2,3,4,"string2"]
f.call(d) { |n| n.is_a?(String) }
#=>[["string", true], ["string2", true]]

Secondary Solution:

However you can also yield inside the lambda if the block is passed to the method instead e.g.

def some_method(data)
  #extended syntax for readability but works fine as 
  #f = lambda{|data,&block| data.select{|m| block.call(m) }.map { |m| [m, block.call(m)]}}
  f = lambda do |data| 
        data.select do |m| 
          yield(m)
        end.map do |m| 
          [m, yield(m)]
        end
      end
  f.call(data)
end
some_method(["string",1,2,3,4,"string2"]) { |s| s.is_a?(String) } 
#=> [["string", true], ["string2", true]]

Tertiary Soultion: (A spawn of the secondary solution that more closely matches your question)

Define a secondary method

def some_method(some_data)
   x = filter(some_data) {|m| m.is_a?(String)}
   y = filter(some_data) {|m| m.is_a?(Fixnum)}
   [x, y]
end  
def filter(data) 
  data.select do |m| 
    yield(m)
  end.map do |m| 
    [m, yield(m)]
  end
end
some_method(["string",1,2,3,4,"string2"])
#=>[
    [["string", true], ["string2", true]], 
    [[1, true], [2, true], [3, true], [4, true]]
   ]

Quaternary Solution:

There is technically a fourth option and I am posting it just for your sake because it represents your original code as closely as possible. It is by far the strangest pattern (almost as strange as the word quaternary) of the 4 but I like to be thorough:

def some_method(some_data)
    f = lambda{|data| data.select{|m| yield(m) }.map { |m| [m, yield(m)]}}
    if block_given?
      f.call(some_data)
    else  
      x = some_method(some_data) {|m| m.is_a?(String)}
      y = some_method(some_data) {|m| m.is_a?(Fixnum)}
      [x,y] 
    end
end
some_method(["string",1,2,3,4,"string2"])
#=>[
    [["string", true], ["string2", true]], 
    [[1, true], [2, true], [3, true], [4, true]]
   ]
engineersmnky
  • 25,495
  • 2
  • 36
  • 52
  • It definitely isn't possible. `yield` only works directly in method (`def`) and singleton class (`class <<`) scope ([see](https://github.com/ruby/ruby/blob/v2_2_2/vm_insnhelper.c#L2071)). – cremno Jun 04 '15 at 17:35
  • 1
    @cremno updated my post you can apparently `yield` from a `lambda` inside a method if that method is passed a block for the `lambda` to `yield` to. So you just cannot `yield` to a block passed directly to a `lambda`. – engineersmnky Jun 04 '15 at 17:52
  • Yes, that you can do. Sorry, my description wasn't precise enough. – cremno Jun 04 '15 at 18:00