Overloading
Function Overloading
Definition of Function Overloading
In Cangjie, function overloading refers to the situation where a function name corresponds to multiple function definitions with different parameter types in a scope.
For details about the definition of function overloading, see Definition of Function Overloading.
Note that:
-
A static member function of the class, interface, or struct type and an instance member function cannot be overloaded.
-
Static member functions and instance member functions in extensions of the same type cannot be overloaded, except when both functions are private in different extensions of the same type.
-
A constructor of the enum type, a static member function, and an instance member function cannot be overloaded.
In the following example, the instance member function f
and static member function f
in class A
are overloaded. As a result, a compilation error is reported.
class A {
func f() {}
//Static member function can not be overloaded with instance member function.
static func f(a: Int64) {} // Error
}
In the following example, the instance member function g
and static member function g
in the extension of class A
are overloaded. As a result, a compilation error is reported.
class A {}
extend A {
func g() {}
static func g(a: Int64) {} // Error
}
In the following example, the instance member function h
and static member function h
are in different extensions of class A
and are both private
. No compilation error is reported.
extend A {
private func h() {}
}
extend A {
private static func h(a: Int64) {} // OK
}
In the following example, the constructor f
, instance member function f
, and static member function f
of enum E
are overloaded. As a result, a compilation error is reported.
enum E {
f(Int64) // constructor
// Instance member function can not be overloaded with constructor.
func f(a: Float64) {} // Error
// Static member function can not be overloaded with instance member function or constructor.
static func f(a: Bool) {} // Error
}
When making a function call, the function definition to be used must be determined based on the types of the arguments and the context in the function call expression. The steps are as follows:
Step 1: Construct a function candidate set.
Step 2: Resolve function overloading.
Step 3: Determine the argument types.
Candidate Set of Overloading Functions
When the function f
is called, it is necessary to first determine which functions can be called. A set of functions that can be called is referred to as a candidate set of overloading functions (referred to as "candidate set"), and is used for function overloading resolution.
To construct a function candidate set, perform the following steps:
- Search for a visible function set. Based on the form of the call expression and the context, identify all the visible functions.
- Perform a type check of function call on functions in the visible function set. Functions that pass the type check (that is, functions that can be called) are added to the candidate set.
Visible Function Set
The visible function set must meet the following rules:
-
Functions are visible in the scope.
-
The visible function set is determined based on whether
f
is a common function or a constructor.2.1. If
f
is a constructor, the visible function set is determined according to the following rules:
-
If
f
is the name of a type with a constructor, the visible function set includes only the constructor defined inf
. -
If
f
is the name of anenum
constructor, the visible function set includes only the constructor namedf
in the specifiedenum
. -
If
f
issuper
, the call expression is within a class or interface, andf
includes only the direct superclass constructor of the class or interface where the call expression is located. -
If
f
is a type name without a constructor, the visible function set is empty.2.2. If
f
is a common function, the visible function set is determined based on whether there is qualified prefix. -
If
f(...)
is directly called by its name without a qualified prefix, the visible function set includes the following functions namedf
:(1) Local functions visible in the scope are included.
(2) If the function call occurs in the static context of a
class
,interface
,struct
,enum
, orextend
, the static member functions of the type are included.(3) If the function call occurs in the non-static context of a
class
,interface
,struct
,enum
, orextend
, the static and non-static member functions of the type are included.(4) Global functions defined in the package where the function call occurs are included.
(5) Functions declared in the extension defined in the package where the function call occurs are included.
(6) Functions imported in the package through
import
where the function call occurs are included. -
For a function call
c.f(...)
with a qualified prefix, the visible function set is determined based on the qualified prefix.(1) If
c
is a package name, the visible function includes only the global functionf
defined inpackage c
, including the functions re-exported throughpublic import
inc
, but not the functions imported only throughimport
inc
.(2) If
c
is a type name defined byclass
,interface
,struct
, orenum
, the visible function set includes only the static member methodf
ofc
.(3) If
c
isthis
, that is,this.f(...)
, the call expression occurs in the definition or extension ofclass
,interface
,struct
, orenum
. If it appears in the type definition, the visible function set includes only the non-static member methodf
in the current type (including the inherited method but excluding the method in the extension). If it appears in a type extension, the visible function set contains the non-static member methodf
in the type and the non-static member methodf
in the extension.(4) If
c
issuper
, that is,super.f(...)
, it indicates that the call expression occurs in aclass
orinterface
, and the visible function set includes only the non-static member methodf
of the superclass or superinterface of the current type.(5) If the function call is in the format of
object name.f (...)
:If the object type contains the member method `f`, the visible function set includes only the member methods named `f`.
2.3. If f
is a type instance, the visible function set is determined based on the ()
operator overloading function defined in its type or type extension.
Assuming that the type of `f` is T, the visible function set includes:
(1) The `()` operator overloading function defined within T.
(2) The `()` operator overloading function defined in the extension of T (visible in the current scope).
(3) If T has a superclass, it also includes the `()` operator overloading function inherited from the superclass.
(4) If T implements an interface, the `()` operator overloading function with default implementations inherited from the interface is also included.
- For the generic function
f
, the generic function that enters the visible function set may be a partially instantiated generic function or a fully instantiated function. The form of the call expression determines which kind of generic functions can enter the visible function set. For details, see [Generic Function Overloading].
Type Check
Type check is performed on functions in the visible function set. Only functions that pass the function call type check can enter the candidate set.
open class Base {}
class Sub <: Base {}
func f<X, Y>(a: X, b: Y) {} // f1, number of type parameters is not matched
func f<X>(a: Base, b: X) where X <: Sub {} // f2
func test() {
f<Sub>(Base(), Sub()) // candidate set: { f2 }
}
Note that:
- If an argument has multiple types, and one of those types can pass the type check, it is considered that the argument can pass the type check of the candidate function.
open class A {}
open class B <: A {}
func g(a: A): B { //g1
B()
}
func g(a: B): B { //g2
B()
}
func g(a: Int64): Unit {} //g3
// (A)->B <: (B)->B <: (B)->A
func f(a: (A)->B) {} //f1, g1 can pass the type check
func f(a: (B)->B) {} //f2, g1, g2 can pass the type check
func f(a: (B)->A) {} //f3, g1, g2 can pass the type check
func f(a: Bool) {} //f4, no g can pass the type check
func test() {
f(g) // candidate set: { f1, f2, f3 }
}
Function Overloading Resolution
If the candidate set is empty, that is, there is no matching item, a compilation error is reported.
If there is only one function in the candidate set, that is, there is only one matching item, the matched function is selected for calling.
If there are multiple functions in the candidate set, there are multiple matching items. The function with the highest scope level in the candidate set is selected based on the rules in [Scope Priority]. If there is only one function with the highest scope level, this function is selected. Otherwise, the best matching item is selected according to the [Best Matching Rules]. If the best matching item cannot be determined, a compilation error is reported.
Scope Priority
The higher the scope level, the higher the priority a function has during function overloading resolution. That is, if two functions are in the candidate set, the one with a higher scope level is selected. If two functions have the same scope level, the best matching item is selected based on the rules outlined in [Best Matching Rules].
The following should be noted regarding scope priority:
-
Functions defined in the superclasses and subclasses in the candidate set are treated as having the same scope priority during function overloading resolution.
-
Functions defined in a type and its extension in the candidate set are treated as having the same scope priority during function overloading resolution.
-
Functions defined in different extensions of the same type are treated as having the same scope priority during function overloading resolution.
-
Operator overloading functions defined in extensions or types and built-in operators are treated as having the same scope priority during function overloading resolution.
-
Apart from the preceding type scope levels, the scope levels of other situations are determined based on the rules in [Scope Levels].
/* According to the scope-level precedence principle, two functions in the candidate set, with different scope-levels, are preferred to the one with higher scope-level. */
func outer() {
func g(a: B) {
print("1")
}
func g(a: Int32) {
print("3")
}
func inner() {
func g(a: A) { print("2") }
func innermost() {
g(B()) // Output: 2
g(1) // Output: 3
}
g(B()) // Output: 2
innermost()
}
inner()
g(B()) // Output: 1
}
In the preceding example, the g
function is called in the innermost
function, and the argument B()
is passed. According to the scope priority rules, the function with a higher scope level is preferentially selected. Therefore, the function g
defined in line 7 is selected.
/* The inherited names are at the same scope level as the names defined or declared in the class. */
open class Father {
func f(x: Child) { print("in Father") }
}
class Child <: Father {
func f(x: Father) { print("in Child") }
}
func display() {
var obj: Child = Child()
obj.f(Child()) // in Father
}
In the preceding example, the display
function calls the f
function and the argument Child()
is passed. The functions in the superclasses and subclasses that constitute overloading has the same scope level. The f(x: Child) {...}
defined in the Father
class is selected based on the best matching rules.
In the following example, the f
function defined in the C
type and the f
function defined in the extension of the C
type are treated as having the same scope priority during function overloading.
open class Base {}
class Sub <: Base {}
class C {
func f(a: Sub): Unit {} // f1
}
extend C {
func f(a: Base): Unit {} // f2
func g() {
f(Sub()) // f1
}
}
var obj = C()
var x = obj.f(Sub()) // f1
Best Matching Rules
The candidate set has multiple functions with the highest priority: { f_1
, ..., f_n
}. These functions are called matching items. If there is one matching item f_m
that is a better match than other matching items, f_m
is the best matching function.
For comparing two matching items and determining which one is a better match, the comparison is based on the parameters of the two matching items and is performed in two steps:
Step 1: Determine the number and sequence of parameters to be compared.
-
If a function parameter has a default value, optional parameters without specified arguments are not compared.
open class Base {} class Sub <: Base {} func f(a!: Sub = Sub(), b!: Int32 = 2) {} // This is f1, func f(a!: Base = Base()) {} // This is f2. var x1 = f(a: Sub()) // parameters involved in comparison in f1 is only a
-
If a function has named parameters, the sequence of arguments may be different from that of parameters. In this case, the sequence of parameters used for comparison must be the same as that of arguments, so that the parameters of each candidate function used for comparison correspond correctly.
Step 2: Compare parameters of the two matching items to determine which one is a better match.
For two matching items f_i
and f_j
, f_i
is considered a better match than f_j
if the following conditions are met:
For a function call expression: e<T1, ..., T_p>(e_1, ...,e_m, a_m+1:e_m+1, a_k:e_k)
, if the number of arguments is k, the number of parameters used for comparison is k.
For the comparison of f_i
and f_j
, the parameters (a_i1, ..., a_ik)
and (a_j1, ..., a_jk)
are compared, and the corresponding parameter types are (A_i1, ..., A_ik)
and (A_j1, ..., A_jk)
.
If passing the parameters (a_i1, ..., a_ik)
of f_i
to f_j
as arguments allows f_j
to pass the function call type check, while passing the parameters (a_j1, ..., a_jk)
of f_j
to f_i
as arguments does not allow f_i
to pass the function call type check, then f_i
is considered a better match than f_j
. The following is an example:
func f_i<X1,..., Xp>(a_i1: A_i1,..., a_ik: A_ik) {// f_i may not have type parameters
f_j(a_i1, ..., a_ik) // If this call expression can pass the type checking
}
func f_j<X1,..., Xq>(a_j1: A_j1,..., a_jk: A_jk) {// f_j may not have type parameters
f_i(a_j1, ..., a_jk) // And this expression cannot pass the type checking
}
The following are some examples of function overloading resolutions:
interface I3 {}
interface I1 <: I2 & I3 {}
interface I2 <: I4 {}
interface I4 <: I3 {}
func f(x: I4) {} // f1
func f(x: I3) {} // f2
class C1 <: I1 {}
var obj = C1()
var result = f(obj) // choose f1, because I4 <: I3
open class C1 {}
open class C2 <: C1 {}
class C3 <: C2 {}
func f(a: C1, b: C2, c: C1) {} // f1
func f(a: C3, b: C3, c: C2) {} // f2
func f(a: C3, b: C2, c: C1) {} // f3
// function call
var x = f(C3(), C3(), C3()) // f2
open class A {}
class B <: A {}
func foo<X>(a: X, b: X): Int32 {} // foo1
func foo(a: A, b: B): Int32 {} // foo2
func foo<X>(a: A, b: X): Int32 {} // foo3
foo(A(), A()) // Error: cannot resolve.
foo(A(), 1) // foo3. foo3 is the only function in candidate set.
Determining Argument Types
If there are multiple types for the arguments:
After the best matching function is determined, if only one type T
among the argument types can pass the type check of the function, the type of the argument is T
. Otherwise, a compilation error is reported.
// Sub <: Base
func f(a: (Base)->Int64, b : Sub) {} //f1
func f(a: (Sub)->Int64, b: Base) {} //f2
func g(a: Base): Int64 { 0 } // g1
func g(a: Sub): Int64 { 0 } // g2
func test() {
f(g, Base()) // Error, both of g can pass f2's type check.
f(g, Sub()) // OK, only g1 passes f1's type check.
}
Operator Overloading
Cangjie defines a series of operators represented by special characters (for details, see [Expressions]), such as arithmetic operators (+
, -
, *
, and /
), logical operators (!
, &&
, ||
), and function call operators (()
). By default, the operands of these operators can only be of specific types. For example, the operands of arithmetic operators can only be of the numeric type, and the operands of logical operators can only be of the Bool
type. If you want to extend some operand types supported by operators or allow custom types to use these operators, you can use operator overloading
.
If you need to overload an operator opSymbol
on a type Type
, you can define an operator function
named opSymbol
for Type
. In this way, when an instance of Type
uses opSymbol
, the operator function named opSymbol
is automatically called.
The syntax for defining an operator function is as follows:
operatorFunctionDefinition
: functionModifierList? 'operator' 'func'
overloadedOperators typeParameters?
functionParameters (':' type)?
(genericConstraints)? ('=' expression | block)?
;
The definition of an operator function is similar to that of a common function. The differences are as follows:
- When defining an operator function, add the
operator
modifier before thefunc
keyword. overloadedOperators
is the operator function name. It must be an operator, and only fixed operators can be overloaded. For details about the operators that can be overloaded, see [Operators That Can Be Overloaded].- The number of parameters of the operator function must be the same as the number of operands required by the operator.
- An operator function can be defined only in class, interface, struct, enum, and extend.
- An operator function has the semantics of an instance member function. Therefore, the
static
modifier cannot be used. - An operator function cannot be a generic function.
In addition, pay attention to the following:
- The inherent priority and law of associativity of an operator is not changed after it is overloaded. For details, see [Expressions].
- Whether a unary operator is used as a prefix operator or a suffix operator is the same as the default usage of this operator. There is no ambiguity because there is no operator in Cangjie that can be used as both a prefix and a suffix.
- The operator function is called in the inherent way of the operator. (That is, the operator function to be called is determined by the number and type of operands.)
If you want to overload an operator opSymbol
for a type Type
, you need to define an operator function named opSymbol
on the type. You can define operator functions in either of the following ways:
-
Use
extend
to add operator functions to types so that operators can be overloaded for these types. This method is required for types that cannot directly contain function definitions (types other thanstruct
,class
,enum
, andinterface
). -
For types that can directly contain function definitions (including class, interface, enum, and struct), define operator functions in the types directly.
Defining Operator Functions
An operator function implements an operator on a specific type. Therefore, the difference between defining an operator function and defining a common instance member function lies in the parameter type convention.
-
For unary operators, operator functions have no parameter, and there is no requirement on the return value type.
For example, assuming that
opSymbol1
is a unary operator, the operator functionopSymbol1
is defined as follows:
operator func opSymbol1(): returnType1 {
functionBody1
}
-
For binary operators, operator functions have only one parameter
right
, and there is no requirement on the return value type.For example, assuming that
opSymbol2
is a binary operator, the operator functionopSymbol2
may be defined as follows:
operator func opSymbol2(right: anyType): returnType2 {
functionBody2
}
Similarly, for a type TypeName
that defines an operator function, these operators may be used on an instance of the TypeName
type like common unary or binary operators (retaining the inherent usage of each operator). In addition, the operator function is an instance function. Therefore, using the overloaded operator opSymbol
on the instance A
of the TypeName
type is actually the syntactic sugar of the function call A.opSymbol(arguments)
(different operator functions are called based on the operator type, number of parameters, and type).
The following example describes how to define an operator function in the class
type to overload a specific operator for the class
type.
Suppose we want to implement the unary minus (-
) and binary addition (+
) operations on a class
type named Point
(containing two member variables x
and y
of the Int32
type). -
obtains negative values of two member variables x
and y
in a Point
instance, and then return a new Point
object. +
sums up the two member variables x
and y
in each Point
instance, and then return a new Point
object.
First, define a class
named Point
, and define operator functions named -
and +
in the class
, as shown in the following:
class Point {
var x: Int32 = 0
var y: Int32 = 0
init (a: Int32, b: Int32) {
x = a
y = b
}
operator func -(): Point {
return Point(-x, -y)
}
operator func +(right: Point): Point {
return Point(x + right.x, y + right.y)
}
}
Then, the -
unary operator and the +
binary operator can be used directly in a Point
instance.
main(): Int64 {
let p1 = Point(8, 24)
let p2 = -p1 // p2 = Point(-8, -24)
let p3 = p1 + p2 // p3 = Point(0, 0)
return 0
}
Scopes of Operator Functions and Search Strategy for Calling Operator Functions
This section describes the scope of the operator function (see [Names, Scopes, Variables, and Modifiers]) and the search strategy when the operator function is called.
Scopes of Operator Functions
An operator function has the same scope level as a common function defined or declared in the same position.
Search Strategy for Calling Operator Functions
This section describes the search strategy when the operator function is called (that is, the operator is used). Only the search strategy of the binary operator (denoted as op
) is described here, because the search of the unary operator function is a sub-case of the search of the binary operator function and follows the same policy.
Step 1: Determine types of the left operand lOperand
and the right operand rOperand
(assuming that their types are lType
and rType
respectively).
Step 2: In the current scope of the call expression lOperand op rOperand
, search for all operator functions whose names are op
and whose right operand type is rType
that are associated with lType
. If there is only one such operator function, the expression call is converted into a call to this operator function. If no such function is found, move on to step 3.
Step 3: Repeat step 2 in a scope with a lower priority. If no matching operator function is found in the lowest-priority scope, the search is terminated and a compilation error ("function not defined") is generated.
Operators That Can Be Overloaded
The following table lists all operators that can be overloaded (in descending order of priority).
Operator | Description |
---|---|
() | Function call |
[] | Indexing |
! | NOT: unary |
- | Negative: unary |
** | Power: binary |
* | Multiply: binary |
/ | Divide: binary |
% | Remainder: binary |
+ | Add: binary |
- | Subtract: binary |
<< | Bitwise left shift: binary |
>> | Bitwise right shift: binary |
< | Less than: binary |
<= | Less than or equal: binary |
> | Greater than: binary |
>= | Greater than or equal: binary |
== | Equal: binary |
!= | Not equal: binary |
& | Bitwise AND: binary |
^ | Bitwise XOR: binary |
` | ` |
Note that:
-
Apart from the operators listed in the preceding table, other operators (see [Operators] for the complete operator list) cannot be overloaded.
-
If a binary operator other than a relational operator (
<
,<=
,>
,>=
,==
, or!=
) is overloaded for a type, and the return type of the operator function is the same as the type of a left operand or its subtype, the type supports the corresponding compound assignment operator. If the return type of an operator function is different from the type of a left operand or its subtype, a type mismatch error is reported when the corresponding compound assignment operator is used. -
Cangjie does not support custom operators. That is, only
operators
in the preceding table can be overloaded. -
There are no specific type requirements for the input parameters or return value when overloading the function call operator (
()
). -
If the
T
type already supports an operator in the preceding table by default, a redefinition error is reported when an operator function with the same signature is implemented for theT
type through extension. For example, overloading arithmetic operators, bitwise operators, or relational operators with the same signature and are already supported by numeric types, overloading relational operators with the same signature forRune
, or overloading logical operators, equal-to operators, or not-equal-to operators with the same signature for the Bool type, will all result in a redefinition error.
Special scenarios are as follows:
-
The
()
operator overloading function cannot be called usingthis
orsuper
. -
For the enum type, the constructor form is preferentially matched when both constructor form and the
()
operator overloading function form are met.
// Scenario for `this` or `super`.
open class A {
init(x: Int64) {
this() // error, missing argument for call with parameter list: (Int64)
}
operator func ()(): Unit {}
}
class B <: A {
init() {
super() // error, missing argument for call with parameter list: (Int64)
}
}
// Scenario for enum constructor.
enum E {
Y | X | X(Int64)
operator func ()(a: Int64){a}
operator func ()(a: Float64){a}
}
main() {
let e = X(1) // ok, X(1) is to call the constructor X(Int64).
X(1.0) // ok, X(1.0) is to call the operator () overloading function.
let e1 = X
e1(1) // ok, e1(1) is to call the operator () overloading function.
Y(1) // oK, Y(1) is to call the operator () overloading function.
}
Index Operator Overloading
Index operators ([]
) have two forms: let a = arr[i]
(for obtaining a value) and arr[i] = a
(for assigning a value). They can be distinguished based on whether they contain the special named parameter value. You are not required to use both forms at the same time for index operator overloading. You can use only either of them as required.
In let a = arr[i]
, a sequence of one or more non-named parameters of any type of the corresponding overloaded operator is enclosed in brackets ([]
). Other named parameters are not allowed. The return type can be any type.
class A {
operator func [](arg1: Int64, arg2: String): Int64 {
return 0
}
}
func f() {
let a = A()
let b: Int64 = a[1, "2"]
// b == 0
}
In arr[i] = a
, a sequence of one or more non-named parameters of any type of the corresponding overloaded operator is enclosed in brackets ([]
). The expression on the right of =
corresponds to the named parameter of the overloaded operator. There can be only one named parameter, which must be named as value, cannot have a default value, and can be of any type. The return type must be Unit.
Note that value is only a special mark and does not need to be called as a named parameter when an index operator is used to assign a value.
class A {
operator func [](arg1: Int64, arg2: String, value!: Int64): Unit {
return
}
}
func f() {
let a = A()
a[1, "2"] = 0
}
In particular, the numeric, Bool, Unit, Nothing, Rune, String, Range, Function, and tuple types do not support overloading index operators.