How inject
works
The block passed to inject
receives two arguments in each iteration. The first argument (prev_nearest_key
here) is an "accumulator" whose value is whatever value was returned by the previous iteration. (For the first iteration it will be the argument given to inject
or, inits absence, the first element of the collection—vertices[0]
here.) The second argument (key
) is the current element of the collection. When iteration is complete, the final value of the accumulator is returned.
When you call next val
in a block passed to an iterator, val
is immediately returned from the block and the next iteration begins. To demonstrate, here's how it looks with map
:
["h", "e", "l", "l", "o"].map do |letter|
next letter.upcase if "aeoiu".include?(letter)
letter
end
# => ["h", "E", "l", "l", "O"]
Above, when letter
is a vowel, letter.upcase
is returned from the block and the next line is never evaluated. When letter
isn't a vowel, it's returned from the block unchanged.
Here's a similar example with inject:
["h", "e", "l", "l", "o"].inject do |accum, letter|
next accum + letter.upcase if "aeoiu".include?(letter)
accum + letter
end
# => "hEllO"
What's different here? When letter
is a vowel, accum + letter.upcase
is returned from the block (effectively appending letter.upcase
to accum
), and the next line is never evaluated. When letter
isn't a vowel, accum + letter
is returned from the block. In both cases, the value returned from the block becomes accum
in the next iteration.
How your code works
Here's your code, but with more intelligible variable names.
nearest_vertex = vertices.inject do |prev_nearest_vertex, current_vertex|
next current_vertex unless distances[prev_nearest_vertex]
next prev_nearest_vertex unless distances[current_vertex]
next prev_nearest_vertex if distances[prev_nearest_vertex] < distances[current_vertex]
current_vertex
end
I've renamed a
, the accumulator, to prev_nearest_vertex
, since it's the value returned by the previous iteration, and b
to current_vertex
.
The first two lines inside the block are just checking to see if distances[prev_nearest_vertex]
and distances[current_vertex]
are nil
. They could (and, perhaps, should) be written like this instead:
next current_vertex if distances[prev_nearest_vertex].nil?
next prev_nearest_vertex if distances[current_vertex].nil?
The first line basically says, "If the previous nearest vertex's distance is nil
, then it's not the nearest, so set prev_nearest_vertex
to current_vertex
in the next iteration." The second line says "If the current vertex's distance is nil
, then it's not the nearest, so keep prev_nearest_vertex
the same in the next iteration.
And here are the third and fourth lines:
next prev_nearest_vertex if distances[prev_nearest_vertex] < distances[current_vertex]
current_vertex
These could be rewritten like this:
if distances[prev_nearest_vertex] < distances[current_vertex]
prev_nearest_vertex
else
current_vertex
end
It just says, "Set prev_nearest_vertex
in the next iteration to prev_nearest_vertex
if its distance is less; otherwise set it to current_vertex
.
This is pretty good code, but you should probably...
Do this instead
Ruby's Enumerable module has a lot of really useful methods, including one called min_by
. It evaluates the given block for each element in an Enumerable and returns the element for which the lowest value was returned. To demonstrate, consider this map
:
words = ["lorem", "ipsum", "dolor", "sit", "amet"]
words.map {|word| word.size }
# => [5, 5, 5, 3, 4]
This just turns an array of words into an array of their sizes. Now suppose we want to get the word that's the shortest. This is easy with min_by
:
words = ["lorem", "ipsum", "dolor", "sit", "amet"]
words.min_by {|word| word.size }
# => "sit"
Instead of returning the words' sizes, this calculates their sizes and then returns the word whose size is the smallest.
This is directly applicable to your code. Again, consider a map
operation:
vertices = [1, 2, 3, 4, 5, 6]
distances = { 1 => 0, 2 => 3, 3 => 2, 4 => 18, 5 => nil, 6 => 7 }
vertices.map do |vertex|
distances[vertex] || Float::INFINITY
end
# => [0, 3, 2, 18, Infinity, 7]
This produces an array of distances corresponding to the elements in vertices
, but nil
distances are replaced with Float::INFINITY
. This is useful because n < Float::INFINITY
is true for every number n
. As before, we can now replace map
with min_by
to get the vertex corresponding to the smallest distance:
def test(vertices, distances)
vertices.min_by {|vertex| distances[vertex] || Float::INFINITY }
end
test(vertices, distances)
# => 1
inject
up. According to your comments, you expect it to be|element, accum|
whilst it is exactly the opposite:|accum, element|
. Try to swap|a, b|
to|b, a|
. – Phan