29

I have an array, and I want the result of the first block that returns a truthy value (aka, not nil). The catch is that in my actual use case, the test has a side effect (I'm actually iterating over a set of queues, and pop'ing off the top), so I need to not evaluate the block beyond that first success.

a,b,c = [1,2,3]

[a,b,c].first_but_value{ |i| (i + 1) == 2 } == 2

a == 2
b == 2
c == 3

Any ideas?

6
  • @sevenseacat, please point out where am I being rude so that I can correct it. Commented Jul 23, 2014 at 2:53
  • 2
    The terminology you are using is unclear to me. What is "the result"? The return value? Or the array element? What is a "result of a block"? Next, the parenthesis after "truthy" is confusing to me, because also false is not truthy / truey. Next, "the test has side effect". I do not know what do you mean by "test". Block evaluation? Next, you "need to not evaluate the block beyond that first success". Isn't this what #find method normally does? I already wrote an answer, but I don't believe that you actually mean what you ask for. Commented Jul 23, 2014 at 2:59
  • 3
    Final remark, += operator in your block does not make sense, since i gets discarded after every block evaluation. Commented Jul 23, 2014 at 3:01
  • @sevenseacat, I rewrote my comments using more formal tone. The content has not changed. Would the tone be OK now? Commented Jul 23, 2014 at 3:03
  • Yes, the == are crude asserts. And I do always forget that Ruby's += doesn't always work the way I expect (hold over from the C++ days), so you're correct that I'm using incorrectly for what I'm trying to achieve. In the actual use case, it's a .pop, but I felt like that would have required additional background. The "result" would definitely be the return value of the block. I don't know any other way to interpret the "result". In my use case, the block will eval to nil rather than false, and not everyone that reads these questions will know nil is falsey Commented Jul 23, 2014 at 18:05

7 Answers 7

29

break is ugly =P

If you want a functional approach, you want a lazy map:

[nil, 1, 2, 3].lazy.map{|i| i && i.to_s}.find &:itself   
# => "1"

If you don't believe it is not iterating throughout the array, just print out and see:

[nil, 1, 2, 3].lazy.map{|i| (p i) && i.to_s}.find  &:itself
# nil
# 1
# => "1"

Replace i.to_s by your block.

Sign up to request clarification or add additional context in comments.

1 Comment

wrong, that's why lazy is there. with lazy you don't have to map all elements. I even provided a version with print so you can test it yourself if you don't believe it.
17
[1, 2, 3].detect { |i| i += 1; break i if i == 2 } 
# => 2

[1, 2, 3].detect { |i| i += 1; break i if i == 10 }
# => nil

5 Comments

Neat, did not know that you can supply a value to break like that (or that'd become the value of the loop expression)
The idea is that in Ruby break and return are essentially the same thing. One is used for loops and the other for function contexts. By breaking the loop we are returning early.. If there was no arg with the break we would simply return nil but with the "return value" we can get the desired return behavior. (The same will work with any loop)
Careful with this, as if nothing found, the return value will be the original array: [1, 2, 3].each{|i| i += 1; break i if i == 10} => [1, 2, 3]
@CarsonReinke easy fix, use #detect instead
@Kache, yeah, it was there all along, strange.
6

find_yield does what you want, check out ruby facets with many core extensions, and especially find_yield Enumberable method: https://github.com/rubyworks/facets/blob/master/lib/core/facets/enumerable/find_yield.rb

1 Comment

Very cool! Pop back on if/when it gets pulled into core ruby, so I can mark this as the answer.
3

Is this what you want to do?

a, b, c = 1, 2, 3

binding.tap { |b|
  break b.local_variable_get [ :a, :b, :c ].find { |sym|
    b.local_variable_set( sym, b.local_variable_get( sym ) + 1 ) == 2
  }
} #=> 2

a #=> 2
b #=> 2
c #=> 3

3 Comments

Ah that's neat. I wouldn't have thought to use binding. Learned something new. See? Wasn't that fun?
I've never encountered binding before, so neat! I'll have to look it up to understand your answer, but thanks!
@diego.greyrobot, I asked Matz to objectify variables quite some time ago. It seems that now we at least got Binding to be explicitly aware of its local variable symbols. I'm still waiting for the = hook :-)
3

Similar to @ribamar's answer, you can alternatively use lazy, filter_map and first:

[nil, 1, 2, 3].lazy.filter_map{|i| i && i.to_s}.first 
# => "1"

I find it easier to read than having to do the find(&:itself).

Note: filter_map was added in Ruby 2.7, so won't work if you need to support old versions of Ruby.

Comments

2

Here's my take, is this closer to your actual use case? Note the content of b is 3 instead of 2 because my_test_with_side_effect is called on b as well.

class MyQueue
  def initialize(j)
    @j = j
  end  
  def my_test_with_side_effect
    (@j+=1) == 2
  end
end

(a,b,c) = [MyQueue.new(1),MyQueue.new(2),MyQueue.new(3)]
[a,b,c].each { |i| break i unless i.my_test_with_side_effect }
=> #<MyQueue:0x007f3a8c693598 @j=3>
a
=> #<MyQueue:0x007f3a8c693980 @j=2>
b
=> #<MyQueue:0x007f3a8c693598 @i=3>
c
=> #<MyQueue:0x007f3a8c693430 @i=3>

Comments

0

I doubt there's a way to do this. The problem being that Ruby creates a closure in the block and the variable i is local to it. Doing i+=1 can be expanded to i = i + 1 which creates a new variable i in the block's scope and doesn't modify the value in any of your a,b,c variables.

9 Comments

So he wants to modify the a, b, c variables? How do you know that? And, btw., in Ruby there is a way to do almost everything. But I need to know what the asker wants.
It look like they might've been doing a crude assert where they wrote a == 2 at the bottom half of the code snippet
Does he actually want to modify the local variables by the block?
Clearly I assumed so. Why don't you ask him yourself?
@BorisStitnicky: Here's my actual use case. I've got a series of generated Resque queues, and I want to get the first job from the set of them. So, if I use the answer from above, what I want is: job_data = queue_names.each{|q| job = Resque.pop(q); break job if job}. Does that tell you why I want to do something like this?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.