Generic Functions

If a function declares one or more type parameters, it is called a generic function. Syntactically, type parameters follow the function name and are enclosed in angle brackets < >. Multiple type parameters are separated by commas ,.

Global Generic Functions

You may declare a global function as generic by using the angle brackets < > enclosing one or more type parameters after the function name. Then you may reference the type parameters in the formal parameters of the function, return type, and function body. For example, the identity function can be defined for any type T as follows:

func id<T>(a: T): T {
    return a
}

In the above definition, <T> is the type parameter of the function, (a: T) is the formal parameter list of the function declaration, in which T is a type variable that corresponds to the type parameter of the id function declaration. The T type variable is then also used as the return type of the id function. Now for any type T there exists a global function id of type (T) -> T that simply returns is parameter.

The next example is more complex. In it, a generic function composition function is defined. That function has three type parameters T1, T2, T3, and is used to compose two functions of types (T1) -> T2 and (T2) -> T3 into one function of the type (T1) -> T3:

func composition<T1, T2, T3>(f: (T1) -> T2, g: (T2) -> T3): (T1) -> T3 {
    return {x: T1 => g(f(x))}
}

Because composition<T1, T2, T3> is a generic function, it may compose two functions of any types, as long as each of them has a single formal parameter and the return value type of the first function is also the type of the sole formal parameter of the other. For instance, (Int32) -> Bool, (Bool) -> Int64 or (Int64) -> Rune, (Rune) -> Int8.

func times2(a: Int64): Int64 {
    return a * 2
}

func plus10(a: Int64): Int64 {
    return a + 10
}

func times2plus10(a: Int64) {
    return composition<Int64, Int64, Int64>(times2, plus10)(a)
}

main() {
  println(times2plus10(9))
  return 0
}

Here, two (Int64) -> Int64 functions are composed. We multiply 9 by 2 and then add 10 to obtain the result 28.

28

Local Generic Functions

A local function can also be generic. For example, the id generic function can be nested and defined in other functions.

func foo(a: Int64) {
    func id<T>(a: T): T { a }

    func double(a: Int64): Int64 { a + a }

    return (id<Int64> ~> double)(a) == (double ~> id<Int64>)(a)
}

main() {
    println(foo(1))
    return 0
}

Due to the unit element property of id, the id<Int64> ~> double and double ~> id<Int64> functions are equivalent, and the result is true.

true

Generic Member Functions

Generic Instance Member Functions

The instance member functions of classes, structs, and enums can be generic. For example:

class C {
    func foo<T>(a: T): Unit where T <: ToString {
        println("${a}")
    }
}

struct S {
    func bar<T>(a: T): Unit where T <: ToString {
        println("${a}")
    }
}

enum E {
    | X | Y

    func baz<T>(a: T): Unit where T <: ToString {
        println("${a}")
    }
}

main() {
    var c = C()
    var s = S()
    var e = E.X
    c.foo(10)        // foo<Int64> is inferred
    s.bar("abc")     // bar<String> is inferred
    e.baz(false)     // baz<Bool> is inferred
    return 0
}

Notes:

  1. The where clauses between the return value types and bodies of the generic member functions are called type constraints. They narrow down the choice of valid type arguments by specifying a superclass and/or one or more interfaces that the respective type argument(s) must respectively inherit or implement. In the above example, where T <: ToString means that the type argument for T must be a type that implements the ToString interface. For details about generic constraints, see Generic Constraints.
  2. The Cangjie compiler may infer the type arguments of generic function calls from actual parameter types in most cases, relieving the developer from the need to them explicitly, as in c.foo<Int64>(10).

The above program outputs the following:

10
abc
false

It is worth noting that the generic instance member functions declared in a class cannot be modified with open, and the following instance member functions cannot be generic at all:

  • Interface member functions, regardless of whether they have a default implementastion,
  • Abstract member functions of abstract classes, and
  • Operator overloading functions:
abstract class A {
    public func foo<T>(a: T): Unit where T <: ToString // Error: abstract generic function not allowed in abstract class
}

open class C <: A {
    public open func bar<T>(a: T): Unit where T <: ToString { // Error: generic function cannot have 'open' modifier
        println("${a}")
    }
}

interface I {
    func baz<T>(a: T): Unit where T <: ToString {  // Error: non-static generic function not allowed in interface
        println("${a}")
    }
    func qux<T>(a: T): Unit where T <: ToString    // Error: non-static generic function not allowed in interface
}

However, it is also important to distinguish between generic member functions and member functions of generic types that do not have their own type parameters:

abstract class A<T> where T <: ToString {
    public func foo(a: T): Unit                  // OK
}

open class C<T> <: A<T> where T <: ToString {
    public open func foo(a: T): Unit {           // OK
        println("${a}")
    }
}

interface I<T> where T <: ToString {
    func bar(a: T): Unit {                       // OK
        println("${a}")
    }
    func baz(a: T): Unit                         // OK
}


class D<T> <: C<T> & I<T> where T <: ToString {
    public func baz(a: T): Unit {                // OK
        println("${a}")
    }
}

main() {
   D().foo(0)
   D().bar("bar")
   D().baz(true)
}

The above program compiles and prints

0
bar
true

When a type is extended using an extend declaration, the functions in the extension can also be generic. For example, we can add a generic member function to the Int64 type.

extend Int64 {
    func printIntAndArg<T>(a: T) where T <: ToString {
        println(this)
        println("${a}")
    }
}

main() {
    var a: Int64 = 12
    a.printIntAndArg<String>("twelve")
}

The program outputs the following.

12
twelve

Generic Static Member Functions

Generic static member functions can be defined in interfaces, classes, structs, enums, and extends. For example, in the following ToPair class, a tuple is returned from ArrayList:

import std.collection.*

class ToPair {
    public static func fromArray<T>(l: ArrayList<T>): (T, T) {
        return (l[0], l[1])
    }
}

main() {
    var res: ArrayList<Int64> = ArrayList([1,2,3,4])
    var a: (Int64, Int64) = ToPair.fromArray<Int64>(res)
    return 0
}