Type inference problem

Hi,

the following code doesn’t work as expected:

class Test
  @data : IO

  def initialize
    file = File.tempfile
    mem = IO::Memory.new
    decision = false

    pp file.size
    pp mem.size

    @data = decision ? file : mem
  end

  def process
    pp @data
    pp @data.class
    pp @data.size
  end
end

Test.new.process

Throws exception:

In src/test.cr:19:14

 19 | pp @data.size
               ^---
Error: undefined method 'size' for IO::ARGF (compile-time type is IO+)

Both types are derived from IO and both have a method ‘size’.
What’s wrong here and what about IO::ARGF?

Thanks in advance.

It works in the initialize method because the compiler knows their specific types File and IO::Memory. However when you try to do the same thing on the @data ivar, it’s not as simple since its typed as IO, which itself does not have a #size method. Compiler tells you this via the compile-time type is IO+ part of the error message. Basically are running into what’s outlined in Virtual and abstract types - Crystal. I.e. @data : File | IO::Memory is reduced to @data : IO+, which now fails since not all IO types implement #size.

EDIT: Related: [RFC] Don't merge unions into parent type when explicitly said so · Issue #9050 · crystal-lang/crystal · GitHub

Just a minor comment, this behaviour is not really caused by virtual and abstract types.
The type restriction of @data is actually IO, thus the parent type of File and IO::Memory. The rest still applies, that IO does not have a size method.

The trouble with virtualization is even if you would declare the ivar type with a union of the specific types (@data : File | IO::Memory), it wouldn’t work because that would then virtualize into IO+.

As a workaround you can explicitly add type guards for @data to make the compiler happy:

if (data = @data) && data.is_a?(File) || data.is_a?(IO::Memory)
  data.size
end

This is quite verbose, unfortunately.
A little more concise alternative is checking for size method:

if (data = @data).responds_to?(:size)
  data.size
end
2 Likes

I am curious - is that the right approach (virtualize that specific union into IO+)?

1 Like