Compiler forcing Array(T | Nil) instead of Array(T)

Hello everyone,

I’m new there, I didn’t publish anything before. I’m sure you won’t be mad at me if I go straight to my problem for now. (: I did not open an issue on Github because I might be doing something wrong.

Let’s consider this simplified extract of my code:

def neighbours(square : Square) : Array(Square)
      [
        square.pos + {0, 1},
        square.pos + {1, 0},
        square.pos - {0, 1},
        square.pos - {1, 0}
      ].map { |pos| find_square(pos) }
       .select { |s| !s.nil? }
    end

The function find_square returns Square | Nil, so the select’s predicate makes sure that we filter any Nil value. Therefore this function will return Array(Square) at runtime.

However I get the following error from the compiler:

Error: method Engine::Game#neighbours must return Array(Engine::Square) but it is returning Array(Engine::Square | Nil)

I don’t really understand. Since the expected type will be just fine at runtime, is the compiler a bit overzealous or am I doing something wrong?

Thanks for your help! (:

A

The gist of it is the type of the array returned via the #map call is Array(Square?). The #select method retains the type of the original array. So even though there won’t actually be any nil values, the array still allows them, thus causing the error.

In this case the solution is to just use Enumerable(T) - Crystal 1.7.2 instead.

def neighbours(square : Square) : Array(Square)
      [
        square.pos + {0, 1},
        square.pos + {1, 0},
        square.pos - {0, 1},
        square.pos - {1, 0}
      ].compact_map { |pos| find_square(pos) }
    end

Welcome to the forum :wave:

In addition to what @Blacksmoke16 said: The recommendation for compact_map is good because that’s the most optimized method. But in order to understand what’s going on, it’s a bit clearer when you realize that compact_map {} is equivalent to map {}.compact. Enumerable#compact removes the Nil type from the collection items. #compact_map combines the functions of #map and #compact because they’re often used together and it’s more performant that way.
Btw. an alternative to compact would be select(Square). This variant of the #select method receives a type as argument which is used as item type of the resulting collection.

2 Likes

Ah! I was indeed doing it wrong. Somehow my eyes refused to see that the #select method was propagating its input type. That makes sense.

Thanks a lot for your time, gentlemen. I’ll retain the #compact_map solution for this specific use case, but I will surely keep the #select(T) in mind for other cases. (:

Problem solved!

A

1 Like