Elegant idiom for this?

From today’s Advent of Code, I needed something like this (hope this little snippet is no spoiler):

  (wire1.points[1..] & wire2.points[1..]).map do |point|
    wire1.points.index(point) + wire2.points.index(point)
  end.min

Since we are iterating over an intersection of items, we positively know the index calls won’t return nil, but the compiler rightly complains about the possibility of adding nils.

Which is an idiomatic way to write that code so that it compiles?

1 Like

I think you could use not_nil!, which will appease the compiler, and will raise at runtime if you feed it an actual Nil

1 Like

There are two main ways to avoid nil warnings. One is to use not_nil! which will explicitly tell the compiler “this thing is not nil!” and raise an exception at runtime if it’s wrong. The better, but slightly less idiomatic way to handle it is to use try. The try method is defined on Object so you can use it on anything. It’s used like this:

# Create a nil value
possibly_nil = nil

[true, false].sample do |bool|
  # This will potentially set the value of `possibly_nil` to 1
  possibly_nil = 1 if bool
end

# This will call `inc` if `possibly_nil` is not nil
puts possibly_nil.try &.inc

# It could also be written like this
puts possibly_nil.try { |i| i.inc }

The latter is definitely preferred, because it won’t throw a runtime exception, but sometimes not_nil! can’t be avoided.

You could actually do this map/reduce behaviour in a more efficient way, eliminating the problem with nilable index. Instead of intersecting the arrays with & you can just iterate both and collect the sum of indices when the elements at both indices match.
That’s going to be much more performant than calculating the intersection first and than searching for the indices again.

What do you mean? Indices do not need to match. The code needs to add the position of the first occurrence of common elements in both arrays (which could be far apart respectively), and return the minimum of those sums.

I could do a first parallel pass to cache point => index and thus reduce the apparent complexity of the index calls. But that all depends on the data size too in practice, since that still incurs the cost of two hash lookups (would need to measure it). I liked the simplicity of the one-liner for a puzzle.

Something like this:

min = wire1.points[1..].each_with_index.min_of do |(point1, i1)|
  if i2 = wire2.points[1..].index(point1)
    i1 + i2
  else
    Int32::MAX
  end
end

This reduces the algorithmic complexity because you don’t need to build an intermediary intersection array. And it’s not much more code than your initial example.

Further optimizations could work without min_of and directly collect min. This would get rid of Int32::MAX and allow to limit the index lookup only look at elements up to min - i1 (and skip entirely if i1 >= min).

3 Likes

That is better.

Handles the “first occurrence” bit for wire1 because i1 is going to be greater for subsequent ones, so i1 + i2 is going to be greater too.

Thanks man!