Best way for Array#find+transform ?

  • Thread starter Jonas Pfenniger (zimbatm)
  • Start date
J

Jonas Pfenniger (zimbatm)

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
 
A

Anurag Priyam

I know I can come up with a new method on Array that would shorten this t=
o:
------
abs_file =3D paths.find_transform do |path|
=A0path =3D File.join(path, filename)
=A0File.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

How about, transforming and then selecting?

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

(Untested)

--=20
Anurag Priyam
http://about.me/yeban/
 
A

Anurag Priyam

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)}
 
J

Jonas Pfenniger (zimbatm)

2011/1/8 Anurag Priyam said:
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 :)
 
W

w_a_x_man

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

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

David J.Hamilton

Excerpts from Jonas Pfenniger (zimbatm)'s message of Sat Jan 08 16:05:05 -0800 2011:
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

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
 
J

Jonas Pfenniger (zimbatm)

2011/1/9 David J. Hamilton said:
You can abuse break to get a +find+ that acts a little like a hypothetica= l
+find_and_map+.

=C2=A0paths =C2=A0 =C2=A0 =3D %w( /usr /usr/local /opt /tmp )

=C2=A0location =C2=A0=3D paths.find do |path|
=C2=A0 =C2=A0candidate =3D "#{path}/whatever"
=C2=A0 =C2=A0break candidate if File.exists? candidate
=C2=A0 =C2=A0false
=C2=A0end

Haha, that's the best-one :) I underestimate the power of break
 
D

David J.Hamilton

Excerpts from Jonas Pfenniger (zimbatm)'s message of Sun Jan 09 04:08:10 -0800 2011:
Haha, that's the best-one :) I underestimate the power of break

My version is slightly verbose. As (e-mail address removed) 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).
 
J

Jonas Pfenniger (zimbatm)

2011/1/10 David J. Hamilton said:
My version is slightly verbose. =C2=A0As (e-mail address removed) implicitly p= ointed out
in their version, the false in the block is actually unnecessary, since t= he
conditional will evaluate to nil, thus allowing the iteration to continue=
 
B

botp

e.

Right, now we have the perfect version :)


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=3D ->{puts "nothing here, passing a null string instead"; ""}
#=3D> #<Proc:0x8cb327c@(irb#1):90 (lambda)>

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

best regards -botp
 
D

David J.Hamilton

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

botp

...wanted to avoid having to write the transformation twice. =A0See his p= revious
example earlier in this thread where he uses find to get the value, and t= hen has
to transform (again) the result.

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

in that case, i'd still prefer something like

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

thanks and best regards -botp
 
R

Robert Klemme

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

in that case, i'd still prefer something like

=A0paths.map{|path| File.join(path,filename)}.find{|path| File.exists?(pa= th)}
=A0#=3D> "/opt/jruby"

Nothing built in but one could do

MapEnum =3D 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 =3D "ls"

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

Cheers

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 
D

David J.Hamilton

Excerpts from botp's message of Tue Jan 11 08:02:45 -0800 2011:
ah, ok (slaps my dummy head after whole thread :)

in that case, i'd still prefer something like

paths.map{|path| File.join(path,filename)}.find{|path| File.exists?(path)}
#=> "/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).
 
B

botp

Whether this is an inelegance is a matter of taste. =A0Personally 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 g= eneral
it is good to know the semantics of break for cases where the transformat= ion
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)}
#=3D> ["/usr", "/usr/jruby"]

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

(0..10).select_and_remap ->(x){x<5}, ->(x){x*x}
#=3D> [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
 
B

botp

=A0paths.find_and_remap ->(path){File.exists?(path)},
->(path){File.join(path,filename)}
=A0#=3D> ["/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) }
#=3D> ["/usr/jruby", "/usr"]

best regards -botp
 
J

Jonas Pfenniger (zimbatm)

2011/1/11 David J. Hamilton said:
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 :)

Cheers!
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,142
Messages
2,570,818
Members
47,362
Latest member
eitamoro

Latest Threads

Top