Generics

Generic functions

Since ooc is strongly typed, usually when definining a function, it will only accept one type of argument:

printInt: func (value: Int) {
  value toString() println()
}

But what if a function is meant to accept various types and react accordingly? Generics can be used for that:

printAnything: func <T> (value: T) {
  value toString() println()
}

Well, that’s a step in the right direction. But it won’t work, because you can’t call methods on generics types. Since T could be anything, from a String to an array to an Int, we can’t make sure it even has a toString method.

What we can do is match on T:

printAnything: func <T> (value: T) {
  match T {
    case Int =>
      value as Int toString() println()
    case =>
      "<unknown>" println()
  }
}

That’s not very convenient - here’s another way to write it:

printAnything: func <T> (value: T) {
  match value {
    case i: Int =>
      i toString()
    case =>
      "<unknown>"
  } println()
}

Inference

Notice how we didn’t have to specify T when calling printAnything, above? That’s because the type of T is inferred. More complex inference is supported as well:

map := HashMap<String, Int> new()
map put("one", 1)
printMap(map)

printMap: func <K, V> (list: HashMap<K, V>) {
  // when called from above, K == String, and V == Int
}

It works for closures as well:

import structs/[ArrayList, List]

map: func <T, U> (list: List<T>, f: Func (T) -> U) -> List<U> {
  copy := ArrayList<U> new()
  for (elem in list) {
    copy add(f(elem))
  }
  copy
}

a := [1, 2, 3] as ArrayList<Int>
b := map(a, |i| i toString())
b join(", ") println()

Here, U is inferred from the return type of the closure.

Generic classes

Above, we have used generic types, such as ArrayList<T> and HashMap<K, V> - how can they be defined? Just like functions, by putting generic type arguments in-between chevrons (<Type1, Type2>) in the class definition:

Container: class <T> {
  t: T

  init: func (=t)
  get: func -> T { t }
  set: func (=t)
}

c := Container<Int> new(24)
c set(12)
c get() toString() println()

Note that inference works here too - since we are passing a T to the constructor, the instantiation part could be simply rewritten as:

c := Container new(24)

Inheritance

Generic types can have subtypes:

ContainerToo: class <T> extends Container<T> {
  print: func {
    match t {
      case i: Int => i toString()
      case => "<unknown>"
    } print()
  }
}

c := ContainerToo new(24)
c print()

Specialization

Specialization happens when a sub-type has fewer type parameters than its super-type:

IntContainer: class extends Container<Int> {
  print: func {
    get() toString() println()
  }
}