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:
- 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 forT
must be a type that implements theToString
interface. For details about generic constraints, see Generic Constraints.- 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
}