soutaro / steep

Static type checker for Ruby
MIT License
1.36k stars 83 forks source link

Filtered collection type inference #1177

Open pointlessone opened 3 months ago

pointlessone commented 3 months ago

Suppose we have the following code:

class Cat
  # ...
  def meow
    # ...
  end
end

def Dog
  # ...
  def bark
    # ...
  end
end

And here's how we might use it:

# pets: Array[Cat | Dog]
def cat song(pets)
  cats = pets.select { |pet| pet.is_a?(Cat) && pet.awake? }
  cats.each do |cat|
    cat.meow # Error here
  end
end

Steep is having issue with this code:

Type `(::Cat | ::Dog)` does not have method `meow`

I guess, this can be justified by the type definition of Array#select but this is not exactly useful. Filtering is used all the time to narrow down collection to a subset with certain properties and type is often that property.

Is there a way to narrow down the type of the collection? Or do I have to rewrite it with next unless pet.is_a?(cat) in theeach` block?

ParadoxV5 commented 12 hours ago

If blocks can have overloads – ideally with ruby/rbs#1875, then RBS can guide type checkers on this type narrowing. Take Enumerable#reject for a nice example:

  def reject: () [F < Elem] { (F) -> false?
                            | (Elem) -> boolish } -> Array[F]
            | () -> ::Enumerator[Elem, ::Array[Elem]]

Relevant methods: