Hello,
I encountered this strange behavior today when trying to combine some filters in a crystal application I’m writing. The code below is a simplified version.
If I use a single instance of a filter or multiple instances of the same filter class, the code fails to compile. However, if I use more than one subclass the code compiles.
I find it a bit strange that the compiler knows that a subclass instance can be added to an Array(Superclass) if there are multiple subclass instances, but fails to do so on a single instance. Is this intended behavior?
I can solve this by “upcasting” each subclass instance, but I find it unintuitive that I only need to this when there is a single subclass present.
struct WorkItem
getter priority : Int32
def initialize(@priority : Int32)
end
end
abstract class Filter
abstract def pass?(item : WorkItem) : Bool
end
class LowPriorityFilter < Filter
def pass?(item : WorkItem) : Bool
item.priority <= 5
end
end
class MediumPriorityFilter < Filter
def pass?(item : WorkItem) : Bool
item.priority >= 6 && item.priority <= 10
end
end
class HighPriorityFilter < Filter
def pass?(item : WorkItem) : Bool
item.priority > 10
end
end
class CombinedFilter < Filter
@filters : Array(Filter)
delegate all?, to: @filters # Just for introspection
def initialize(*filters : Filter)
@filters = filters.to_a
end
def pass?(item : WorkItem) : Bool
@filters.any? { |filter| filter.pass?(item) }
end
end
workitems = [
WorkItem.new(3),
WorkItem.new(5),
WorkItem.new(7),
WorkItem.new(9),
WorkItem.new(11),
WorkItem.new(13)
]
# Using more than one filter works without issues
working_combined_filter = CombinedFilter.new(LowPriorityFilter.new, MediumPriorityFilter.new)
working_filtered_items = workitems.select { |item| working_combined_filter.pass?(item) }
puts "All filters inherit from Filter: #{working_combined_filter.all? { |filter| filter.is_a?(Filter) }}"
p working_filtered_items
# => [WorkItem(@priority=3), WorkItem(@priority=5), WorkItem(@priority=7), WorkItem(@priority=9)]
# Using a single filter fails to compile
# nonworking_combined_filter = CombinedFilter.new(HighPriorityFilter.new)
# In inheritance_test.cr:32:9
#
# 33 | @filters = filters.to_a
# ^-------
# Error: instance variable '@filters' of CombinedFilter must be Array(Filter), not Array(HighPriorityFilter)
# nonworking_filtered_items = workitems.select { |item| nonworking_combined_filter.pass?(item) }
# puts "All filters ingerit from Filter: #{working_combined_filter.all? { |filter| filter.is_a?(Filter) }}"
# p nonworking_filtered_items