Doubts about virtual type

Perhaps we should mask the concept of virtual type at the user-level, which is somewhat confusing.

For example, Virtual and abstract types

@pet is declared as an Animal, but automatically changed to Animal+, and typeof(@pet) returns Animal.

The documentation here doesn’t seem to be correct: Union of classes and structs under the same hierarchy

I think that virtual type is compile-level concept that should not be exposed to the user-level.

It may be simpler to tell the user that “objects of a derived class may be treated as objects of a base class”.

Think you might got a point there.

Also:

You can see that @pet is Animal+. The + means it’s a virtual type, meaning “any class that inherits from Animal, including Animal”.

But Animal means “any class that inherits from Animal, including Animal” too? At least in regular code, not familiar with tool hierarchy

Virtual type is similar to polymorphism in other language, like C# or Java.
Virtual type sometimes brings some confusion, for example:

class Animal
end

class Person
  getter pet

  def initialize(@pet : Animal)
  end
end

crystal tool hierarchy:

- class Object (4 bytes)
  |
  +- class Reference (4 bytes)
     |
     +- class Person (16 bytes)
            @pet : Animal (8 bytes)

if we define Cat or Dog class some places, then crystal tool hierarchy:

- class Object (4 bytes)
|
+- class Reference (4 bytes)
	|
	+- class Person (16 bytes)
			@pet : Animal+ (8 bytes)

The type of @pet changed from Animal to Animal+ simply because we defined the Cat or Dog class.

typeof([Animal.new])                                   # => Array(Animal)
typeof([Cat.new, Dog.new])                             # => Array(Animal)
(typeof([Animal.new])) == (typeof([Cat.new, Dog.new])) # => true

But typeof returns a regular type Animal, not Animal+.

Another example:

module Foo
end

module Bar
end

class Class1
  include Foo
end

class Class2
  include Foo
  include Bar
end

array = [Class1.new, Class2.new] of Foo
typeof(xs) # => Array(Foo)

typeof returns a regular type Foo, not Foo+.

So I think virtual type is a type inference technique for compilers and should not be exposed at the user-level, just tell the user that “objects of a derived class may be treated as objects of a base class”.

I found a related issue:
Internal compiler error from conflict between T* and T+* · Issue #12021 · crystal-lang/crystal (github.com)
My point of view is similar to @asterite 's:

I think everything will be simpler if we stop making the distinction between virtual and non-virtual types, even though it might end up in slightly less performant code. I’m also sure LLVM will notice what’s the effective type and optimize it. The main issue is that the semantic changes: if Bar overrides a method and returns a different type, it affects the type of the parent call seen as a virtual type. But I think that’s a reasonable thing to expect (it works like that in every other statically typed language)