Best way for Array#find+transform?

There is a pattern that I’m using quite regularly, but I’m not
satisfied by my implementation.


filename = “something”
paths = ["/usr", “/usr/local”, “/opt”]
abs_file = nil
paths.each do |path|
path = File.join(path, filename)
File.exist?(path)
abs_file = path
break
end
end

I know I can come up with a new method on Array that would shorten this
to:


abs_file = paths.find_transform do |path|
path = File.join(path, filename)
File.exist?(path) && path
end

But is there an existing method that I oversaw that does about the same
?

  1. Stop iterating if the element is found
  2. Return the transformed element

Cheers,
zimbatm

  1. Stop iterating if the element is found
  2. Return the transformed element

How about, transforming and then selecting?

paths.map{|path| File.join(path, filename)}.select{|name|
File.exist?(path)}

(Untested)

paths.map{|path| File.join(path, filename)}.select{|name| File.exist?(path)}

Of course, I meant to use name, instead of path in the last section. So:

file = paths.map{|path| File.join(path, filename)}.select{|name|
File.exist?(name)}

2011/1/8 Anurag P. [email protected]:

paths.map{|path| File.join(path, filename)}.select{|name| File.exist?(path)}

Of course, I meant to use name, instead of path in the last section. So:

file = paths.map{|path| File.join(path, filename)}.select{|name|
File.exist?(name)}

Yes, if you replace #select by #find, then it is correct, you’ll get
the same result as my algorithm. But if the result is in the first
entry, then all subsequent paths are unnecessarily joined.

Another version of the algorithm would be:

path = paths.find{|p| File.exist?(File.join(p, filename)) }
if path
abs_path = File.join(path, filename)
end

In this version, not all the paths are joined, but File.join is called
two times on the resulting path if any.

In any case, it is not a life-or-death issue, but any ways I look at
it, there is still that small imperfection that annoys me :slight_smile:

Excerpts from Jonas P. (zimbatm)'s message of Sat Jan 08 16:05:05
-0800 2011:

entry, then all subsequent paths are unnecessarily joined.

Another version of the algorithm would be:

path = paths.find{|p| File.exist?(File.join(p, filename)) }
if path
abs_path = File.join(path, filename)
end

You can abuse break to get a +find+ that acts a little like a
hypothetical
+find_and_map+.

paths = %w( /usr /usr/local /opt /tmp )

location = paths.find do |path|
candidate = “#{path}/whatever”
break candidate if File.exists? candidate
false
end

On Jan 8, 5:38pm, “Jonas P. (zimbatm)” [email protected]
wrote:

abs_file = path
File.exist?(path) && path
end

But is there an existing method that I oversaw that does about the same ?

  1. Stop iterating if the element is found
  2. Return the transformed element

Cheers,
zimbatm

filename = “something”
paths = ["/usr", “/usr/local”, “/opt”]
abs_file = paths.each{|path|
path = ( File.join( path, filename) )
break path if File.exist?(path)
}

If the file doesn’t exist anywhere, abs_file will be equal
to the paths array.

2011/1/9 David J. Hamilton [email protected]:

You can abuse break to get a +find+ that acts a little like a hypothetical
+find_and_map+.

paths = %w( /usr /usr/local /opt /tmp )

location = paths.find do |path|
candidate = “#{path}/whatever”
break candidate if File.exists? candidate
false
end

Haha, that’s the best-one :slight_smile: I underestimate the power of break

Excerpts from Jonas P. (zimbatm)'s message of Sun Jan 09 04:08:10
-0800 2011:

end

Haha, that’s the best-one :slight_smile: I underestimate the power of break

My version is slightly verbose. As [email protected] implicitly
pointed out
in their version, the false in the block is actually unnecessary, since
the
conditional will evaluate to nil, thus allowing the iteration to
continue.

Break is indeed nice. And of course you can sometimes do the same thing
with
return, although I think it’s usually much cleaner to break out of
blocks rather
than returning out of them (and the method in which they’re evaluated).

2011/1/10 David J. Hamilton [email protected]:

My version is slightly verbose. As [email protected] implicitly pointed out
in their version, the false in the block is actually unnecessary, since the
conditional will evaluate to nil, thus allowing the iteration to continue.

Right, now we have the perfect version :slight_smile:

Break is indeed nice. And of course you can sometimes do the same thing with
return, although I think it’s usually much cleaner to break out of blocks rather
than returning out of them (and the method in which they’re evaluated).

Yeah I agree, return is less clean because it doesn’t express the
intent of exiting that particular algorithm.

Thanks all for your participation !

Cheers,
zimbatm

On Mon, Jan 10, 2011 at 11:14 PM, Jonas P. (zimbatm)
[email protected] wrote:

Yeah I agree, return is less clean because it doesn’t express the
intent of exiting that particular algorithm.

no on all. on your case, one does not need break nor return. #find
will shortcircuit and returns the first find, or nil otherwise. iow,
the ff should do,

paths.find{|path| File.exists?(File.join(path,filename))}

in fact, find is quite flexible, it allows you to exec/return
something (override) the returned nil by passing a lambda/proc. eg,

nothing= ->{puts “nothing here, passing a null string instead”; “”}
#=> #<Proc:[email protected](irb#1):90 (lambda)>

paths.find(nothing){|path| File.exists?(File.join(path,filename))}
nothing here, passing a null string instead
#=> “”

best regards -botp

On Tue, Jan 11, 2011 at 2:49 PM, David J. Hamilton [email protected]
wrote:

…wanted to avoid having to write the transformation twice. See his previous
example earlier in this thread where he uses find to get the value, and then has
to transform (again) the result.

ah, ok (slaps my dummy head after whole thread :slight_smile:

in that case, i’d still prefer something like

paths.map{|path| File.join(path,filename)}.find{|path|
File.exists?(path)}
#=> “/opt/jruby”

thanks and best regards -botp

Excerpts from botp’s message of Mon Jan 10 18:48:14 -0800 2011:

no on all. on your case, one does not need break nor return. #find
will shortcircuit and returns the first find, or nil otherwise. iow,
the ff should do,

paths.find{|path| File.exists?(File.join(path,filename))}

This is not quite what Jonas wanted. He did not want the first element
for
which the block evaluated to a truthy value; he wanted that element
transformed,
but the transformation was exactly what was needed for the test, and
Jonas
wanted to avoid having to write the transformation twice. See his
previous
example earlier in this thread where he uses find to get the value, and
then has
to transform (again) the result.

Break was not used to achieve short circuiting, which as you note find
already
does, but to specify the value of the entire expression.

Excerpts from botp’s message of Tue Jan 11 08:02:45 -0800 2011:

#=> “/opt/jruby”
Yes, Anurag suggested transforming and then selecting (as you have done,
above).
Jonas’s complaint here is that much of the transformation is
unnecessary,
specifically every transformation that occurs after the value to be
searched
for.

Whether this is an inelegance is a matter of taste. Personally I find
code that
does more than it needs to a little confusing, or at least inelegant.

As a matter of performance, it may not matter much in this case, but in
general
it is good to know the semantics of break for cases where the
transformation
itself is expensive, or if the Enumerable is very large (say, infinite).

On Tue, Jan 11, 2011 at 5:02 PM, botp [email protected] wrote:

#=> “/opt/jruby”
Nothing built in but one could do

MapEnum = Struct.new :enum, :args, :map do
include Enumerable

def each
enum.send(*args) do |*x|
yield map[*x]
end
end
end

class Object
def map_enum_for(*a, &conv)
MapEnum.new(self, a, conv)
end
end

filename = “ls”

p ["/usr", “/usr/local”, “/opt”, “/usr/bin”].map_enum_for(:each) {|x|
File.join(x,filename)}.find {|x| File.exist? x}

Cheers

robert

On Wed, Jan 12, 2011 at 1:03 AM, David J. Hamilton [email protected]
wrote:

Whether this is an inelegance is a matter of taste. Personally I find code that
does more than it needs to a little confusing, or at least inelegant.

indeed

As a matter of performance, it may not matter much in this case, but in general
it is good to know the semantics of break for cases where the transformation
itself is expensive, or if the Enumerable is very large (say, infinite).

not sure if this heps the op, but i had something like this in my
little lib. the find just uses each w a break, but the break does not
return anything (bad experience getting caught w a nasty bug hidden on
a break). anyway, it uses lambdas to specify what you want to
find/select, and then remap. like so,

paths.find_and_remap ->(path){File.exists?(path)},
->(path){File.join(path,filename)}
#=> ["/usr", “/usr/jruby”]

(0…10).find_and_remap ->(x){x>5}, ->(x){x*x}
#=> [6, 36]

(0…10).select_and_remap ->(x){x<5}, ->(x){x*x}
#=> [0, 1, 4, 9, 16]

the mapping params are optional though.

note, i seldom use these because my simple brain prefers to separate
the mapping from the selecting… my main use for them was for
debugging/tracing purposes, eg, when the proc finds something, i then
let it do some other complex things to verify…

best regards -botp

2011/1/11 David J. Hamilton [email protected]:

Jonas’s complaint here is that much of the transformation is unnecessary,
specifically every transformation that occurs after the value to be searched
for.

Yes, I’m a perfectionist ! And thanks for supporting that thread that
I started :slight_smile:

Cheers!

On Wed, Jan 12, 2011 at 12:40 PM, botp [email protected] wrote:

paths.find_and_remap ->(path){File.exists?(path)},
->(path){File.join(path,filename)}
#=> ["/usr", “/usr/jruby”]

i just noticed i had 3 versions more… fwiw :))

paths.find_and_remap ->(path){File.join(path,filename)} {|path|
File.exists?(path) }
#=> ["/usr/jruby", “/usr”]

best regards -botp