Did you notice that in all of the previous examples we never said the types of a
@age? This is because the compiler inferred them for us.
When we wrote:
class Person getter name def initialize(@name) @age = 0 end end john = Person.new "John" john.name #=> "John" john.name.size #=> 4
Since we invoked
Person.new with a
String argument, the compiler makes
@name be a
If we had invoked
Person.new with another type,
@name would have taken a different type:
one = Person.new 1 one.name #=> 1 one.name + 2 #=> 3
If you compile the previous programs with the
tool hierarchy command, the compiler will show you a hierarchy graph with the types it inferred. In the first case:
- class Object | +- class Reference | +- class Person @name : String @age : Int32
In the second case:
- class Object | +- class Reference | +- class Person @name : Int32 @age : Int32
What happens if we create two different people, one with a
String and one with an
Int32? Let's try it:
john = Person.new "John" one = Person.new 1
Invoking the compiler with the
tool hierarchy command we get:
- class Object | +- class Reference | +- class Person @name : (String | Int32) @age : Int32
We can see that now
@name has a type
(String | Int32), which is read as a union of
Int32. The compiler made
@name have all types assigned to it.
In this case, the compiler will consider any usage of
@name as always being either a
String or an
Int32 and will give a compile time error if a method is not found for both types:
john = Person.new "John" one = Person.new 1 # Error: undefined method 'size' for Int32 john.name.size # Error: no overload matches 'String#+' with types Int32 john.name + 3
The compiler will even give an error if you first use a variable assuming it has a type and later you change that type:
john = Person.new "John" john.name.size one = Person.new 1
Gives this compile-time error:
Error in foo.cr:14: instantiating 'Person:Class#new(Int32)' one = Person.new 1 ^~~ instantiating 'Person#initialize(Int32)' in foo.cr:12: undefined method 'size' for Int32 john.name.size ^~~~~~
That is, the compiler does global type inference and tells you whenever you make a mistake in the usage of a class or method. You can go ahead and put a type restriction like
def initialize(@name : String), but that makes the code a bit more verbose and also less generic: everything will work just fine if you create
Person instance with types that have the same interface as a
String, as long as you use a
Person's name like if it were a
If you do want to have different
Person types, one with
@name being an
Int32 and one with
@name being a
String, you must use generics.
If an instance variable is not assigned in all of the
initialize defined in a class, it will be considered as also having the type
class Person getter name def initialize(@name) @age = 0 end def address @address end def address=(@address) end end john = Person.new "John" john.address = "Argentina"
The hierarchy graph now shows:
- class Object | +- class Reference | +- class Person @name : String @age : Int32 @address : String?
You can see
String?, which is a short form notation of
String | Nil. This means that the following gives a compile time error:
# Error: undefined method 'size' for Nil john.address.size
To deal with
Nil, and generally with union types, you have several options: use an if var, if var.is_a?, case and is_a?.
Instance variables can also be initialized outside
class Person @age = 0 def initialize(@name) end end
This will initialize
@age to zero in every constructor. This is useful to avoid duplication, but also to avoid the
Nil type when reopening a class and adding instance variables to it.
In certain cases you want to tell the compiler to fix the type of an instance variable. You can do this with
class Person @age : Int32 def initialize(@name) @age = 0 end end
In this case, if we assign something that's not an
@age, a compile-time error will be issued at the assignment location.
Note that you still have to initialize the instance variables, either with a catch-all initializer or within an
initialize method: there are no "default" values for types.