Generics

Generics allow you to parameterize a type based on other type. Consider a Box type:

  1. class MyBox(T)
  2. def initialize(@value : T)
  3. end
  4. def value
  5. @value
  6. end
  7. end
  8. int_box = MyBox(Int32).new(1)
  9. int_box.value # => 1 (Int32)
  10. string_box = MyBox(String).new("hello")
  11. string_box.value # => "hello" (String)
  12. another_box = MyBox(String).new(1) # Error, Int32 doesn't match String

Generics are especially useful for implementing collection types. Array, Hash, Set are generic types, as is Pointer.

More than one type parameter is allowed:

  1. class MyDictionary(K, V)
  2. end

Any name can be used for type parameters:

  1. class MyDictionary(KeyType, ValueType)
  2. end

Generic class methods

Type restrictions in a generic type’s class method become free variables when the receiver’s type arguments were not specified. Those free variables are then inferred from a call’s arguments. For example, one can also write:

  1. int_box = MyBox.new(1) # : MyBox(Int32)
  2. string_box = MyBox.new("hello") # : MyBox(String)

In the above code we didn’t have to specify the type arguments of MyBox, the compiler inferred them following this process:

  • The compiler generates a MyBox.new(value : T) method, which has no explicitly defined free variables, from MyBox#initialize(@value : T)
  • The T in MyBox.new(value : T) isn’t bound to a type yet, and T is a type parameter of MyBox, so the compiler binds it to the type of the given argument
  • The compiler-generated MyBox.new(value : T) calls MyBox(T)#initialize(@value : T), where T is now bound

In this way generic types are less tedious to work with. Note that the #initialize method itself does not need to specify any free variables for this to work.

The same type inference also works for class methods other than .new:

  1. class MyBox(T)
  2. def self.nilable(x : T)
  3. MyBox(T?).new(x)
  4. end
  5. end
  6. MyBox.nilable(1) # : MyBox(Int32 | Nil)
  7. MyBox.nilable("foo") # : MyBox(String | Nil)

In these examples, T is only inferred as a free variable, so the T of the receiver itself remains unbound. Thus it is an error to call other class methods where T cannot be inferred:

  1. module Foo(T)
  2. def self.foo
  3. T
  4. end
  5. def self.foo(x : T)
  6. foo
  7. end
  8. end
  9. Foo.foo(1) # Error: can't infer the type parameter T for the generic module Foo(T). Please provide it explicitly
  10. Foo(Int32).foo(1) # OK

Generic structs and modules

Structs and modules can be generic too. When a module is generic you include it like this:

  1. module Moo(T)
  2. def t
  3. T
  4. end
  5. end
  6. class Foo(U)
  7. include Moo(U)
  8. def initialize(@value : U)
  9. end
  10. end
  11. foo = Foo.new(1)
  12. foo.t # Int32

Note that in the above example T becomes Int32 because Foo.new(1) makes U become Int32, which in turn makes T become Int32 via the inclusion of the generic module.

Generic types inheritance

Generic classes and structs can be inherited. When inheriting you can specify an instance of the generic type, or delegate type variables:

  1. class Parent(T)
  2. end
  3. class Int32Child < Parent(Int32)
  4. end
  5. class GenericChild(T) < Parent(T)
  6. end

Generics with variable number of arguments

We may define a Generic class with a variable number of arguments using the splat operator.

Let’s see an example where we define a Generic class called Foo and then we will use it with different number of type variables:

  1. class Foo(*T)
  2. getter content
  3. def initialize(*@content : *T)
  4. end
  5. end
  6. # 2 type variables:
  7. # (explicitly specifying type variables)
  8. foo = Foo(Int32, String).new(42, "Life, the Universe, and Everything")
  9. p typeof(foo) # => Foo(Int32, String)
  10. p foo.content # => {42, "Life, the Universe, and Everything"}
  11. # 3 type variables:
  12. # (type variables inferred by the compiler)
  13. bar = Foo.new("Hello", ["Crystal", "!"], 140)
  14. p typeof(bar) # => Foo(String, Array(String), Int32)

In the following example we define classes by inheritance, specifying instances for the generic types:

  1. class Parent(*T)
  2. end
  3. # We define `StringChild` inheriting from `Parent` class
  4. # using `String` for generic type argument:
  5. class StringChild < Parent(String)
  6. end
  7. # We define `Int32StringChild` inheriting from `Parent` class
  8. # using `Int32` and `String` for generic type arguments:
  9. class Int32StringChild < Parent(Int32, String)
  10. end

And if we need to instantiate a class with 0 arguments? In that case we may do:

  1. class Parent(*T)
  2. end
  3. foo = Parent().new
  4. p typeof(foo) # => Parent()

But we should not mistake 0 arguments with not specifying the generic type variables. The following examples will raise an error:

  1. class Parent(*T)
  2. end
  3. foo = Parent.new # Error: can't infer the type parameter T for the generic class Parent(*T). Please provide it explicitly
  4. class Foo < Parent # Error: generic type arguments must be specified when inheriting Parent(*T)
  5. end