Functions
A function is an independent code snippet that completes a specific task. It can be identified by a function name, which can be used for calling a function. In Cangjie, functions are first-class citizens. Functions support variable assignment, and can be used as parameters or return values.
Function Definition
In Cangjie, function definition must comply with the following syntax rules:
functionDefinition
: functionModifierList? 'func'
identifier
typeParameters?
functionParameters
(':' type)?
(genericConstraints)?
block
;
The summary is as follows:
- The keyword
func
is used to define a function. func
can be preceded by a function modifier.func
must be followed by the function name.- The optional type parameter list is enclosed by
<>
. Multiple type parameters are separated by,
. - The function parameters are enclosed by
()
. Multiple parameters are separated by,
. The parameter type of each parameter must be specified. For details, see [Parameters]. - The default function return type is specified by
:type
. - When a function is defined, a function body, which is a block (excluding function parameters), must be specified.
The following example shows all elements of a complete function definition. The access modifier public
is not used, indicating that the function can be accessed in the package. The function is named foo
, has a parameter a
of the Int64
type, a return type Int64
, and a function body.
func foo(a: Int64): Int64 { a }
A function name cannot be assigned a value. That is, the function name cannot appear in the program as the lvalue of an expression.
The operation in the following example is forbidden.
func f(a: Int64): Int64 { a }
// f = {a: Int64 => a + 1} // compile-time error
Function Modifiers
Modifier of Global Function
Global functions can be modified by all access modifiers. The default accessibility is internal
. For details, see Access Modifiers in [Packages and Modules].
Modifiers of Local Functions
No modifier is available for the local function.
Modifiers of Member Functions
The modifiers of class member functions include public
, protected
, private
, internal
, static
, open
, override
, and redef
. For details, see [Members of a Class] and Access Modifiers in [Packages and Modules].
The modifiers of interface member functions include static
and mut
. For details, see [Interface Members].
The modifiers of struct member functions include mut
, public
, private
, internal
, and static
. For details, see [Struct Type].
The modifiers of enum member functions include public
, private
, internal
, and static
. For details, see [Enum Type].
If no access modifier is provided, member functions other than interface member functions can be accessed in the current package and subpackages. Interface member functions contain the semantics of public
by default.
Parameters
When a function is defined, the sequence of parameters in the parameter list is non-named parameters followed by named parameters (including named parameters with and without default values). The syntax of the parameter list is defined as follows:
functionParameters
: ('(' (unnamedParameterList (',' namedParameterList)? )? ')')
| ('(' (namedParameterList)? ')')
;
nondefaultParameterList
: unnamedParameter (',' unnamedParameter)* (',' namedParameter)*
| namedParameter (',' namedParameter)*
;
namedParameterList
: (namedParameter | defaultParameter) (',' (namedParameter | defaultParameter))*
;
namedParameter
: identifier '!' ':' type
;
defaultParameter
: identifier '!' ':' type '=' expression
;
unnamedParameterList
: unnamedParameter (',' unnamedParameter)*
;
unnamedParameter
: (identifier | '_') ':' type
;
For non-named parameters, you can use a _
to replace the parameters that are not used in a function body.
func bar(a: Int64 , b!: Float64 = 1.0, s!: String) {} // OK
func bar2(a: Int64 = 1, b!: Float64 = 1.0, s: String = "Hello") {} // Error
func foo(a: Int64, b: Float64)(s: String = "Hello") {} // Error
func f1(a: Int64, _: Int64): Int64 {
return a + 1
}
func f2(_: String): Unit {
print("Hello Cangjie")
}
All function parameters are immutable variables, that is, they are modified by let
and cannot be assigned values in a function definition.
func foo(a: Int64, b: Float64) {
a = 1 // Error: the parameter 'a' is immutable, and cannot be assigned.
b = 1.0 // Error: the parameter 'b' is immutable, and cannot be assigned.
}
The parameter type of a function is not affected by whether the parameter is a named parameter or whether the parameter has a default value. In addition, when the parameter type is discussed, a type and its alias are considered to be the same type.
Naming Parameters
When defining a function, add !
to the end of a parameter name to define a named parameter.
namedParameter
: identifier '!' ':' type
;
- During function definition, non-named parameters are not allowed after named parameters.
func add1(a: Int32, b!: Int32): Int32 { a + b } // ok
func add2(a!: Int32, b: Int32): Int32 { a + b } // error
- When a parameter is defined as a named parameter and this function is called, you must use the
parameter name:
prefix before the argument value to specify the parameter corresponding to the argument. Otherwise, a compilation error is reported.
func add(a: Int32, b!: Int32): Int32 { a + b }
add(1, b: 2) // ok
add(1, 2) // error
-
If an abstract function or a function modified by
open
has a named parameter, the implementation function or the function modified byoverride
must retain the same named parameter.open class A { public open func f(a!: Int32): Int32 { return a + 1 } } class B <: A { public override func f(a!: Int32): Int32 { // ok return a + 1 } } class C <: A { public override func f(b!: Int32): Int32 { // error return b + 1 } }
Default Values of Parameters
Function parameters can have default values. You can use =
to define default values for parameters. After the default value is defined for a parameter, it can be ignored when that function is called. The function body uses the default value. For better understanding, parameters with default values are called optional parameters.
When a function is defined, an optional parameter is a named parameter. The !
must be added to the end of the optional parameter name. Otherwise, an error is reported during compilation.
defaultParameter
: identifier '!' ':' type '=' expression
;
In the following example, an add
function is defined, and its parameter type list is (Int32, Int32)
. When the function is defined, b
has the default value 1
. Therefore, when the add
function is called and only the value 3
is passed, 3
is assigned to a
, and the result 4
is returned.
If 3
and 2
are passed, the value of b
is 2
.
func add(a: Int32, b!: Int32 = 1): Int32 { a + b }
add(3) // invoke add(3, 1), return 4
add(3, b: 2) // return 5
-
Functions that are modified by the
open
keyword in a class or interface cannot have optional parameters. -
The operator function cannot contain optional parameters.
-
Anonymous functions (lambda expressions) do not allow optional parameters.
-
The name introduced in the default value of a function parameter is obtained from the static scope. That is, the introduced name is a name that can be accessed when the function is defined.
-
The name introduced in the default value of a function parameter can be accessed when the function is defined, and does not need to be consistent with the accessibility of the function itself.
-
The function parameter and its default value do not belong to the function body.
-
The default value of a parameter is evaluated when the function is called, not when the function is defined.
-
When a function is called, parameters are evaluated from left to right based on the definition sequence. The default value of a function parameter can reference the parameters defined before it.
-
When a function is called using its function name, the default value can be used. When a function is called using its variable name, the default value cannot be used.
// Compile-time error. // func f(a: Int32, b!: Int32 = 2, c: Int32): Int32 { ... } // OK. func f1(a: Int32, b: Int32, c!: Int32 = 3, d!: Int32 = 4): Int32 { a + b + c + d } func test() { f1(1, 2) // 10, f1(1, 2, 3, 4) f1(1, 2, c: 5) // 12, f1(1, 2, 5, 4) }
/* The names introduced in the default value, does not need to have the same or least restrictive accessibility of the function. */ var x = 10 var y = 10 func g() {} public func f2(a!: Int64 = x * 2 + y, b!: ()->Unit = g) {} // OK. class AAA { static private var x = 10 func f(a!: Int64 = x) { // OK, public method can use private static field. print("${a}") x = x + 10 print("${x}") } }
/* When a function is called, the name in the function declaration can use the default value. When a function is called by using a variable name, arguments cannot be optional. */ func f1(): (Int64) -> Unit { g1 } func g1(a!: Int64 = 42) { print("g1: ${a}") } let gg1 = f1() let x = gg1() // Error, cannot ommit the argument. let gg3 = g1 let a = gg3() // Error, cannot ommit the argument.
The parameters of a function and their default values do not belong to the function body. Therefore, the return expression in the following example does not contain the function body that surrounds the expression. That is, the return expression does not belong to the outer function f
(because the definition of the inner function g
has taken effect) or the function body of the inner function g
.
func f() {
func g(x! :Int64 = return) { // Error: return must be used inside a function body
0
}
1
}
Function Body
The function body consists of a block.
Local Variables
In Cangjie, variables can be defined in the function body, which are called local variables.
A variable can be modified as a mutable variable by using var
or as an immutable variable by using let
.
func foo(): Unit {
var a = 1
let b = 1
}
Nested Functions
In Cangjie, functions can be defined in the function body, which is called nested functions. Nested functions can capture variables defined in external functions or other nested functions. Nested functions support recursion.
func foo(): Unit {
func add(a: Int32, b: Int32) { a + b }
let c = 1
func nest(): Unit {
print("${c}") // 1
var b = add(1, 2) // b = 3
}
}
Return Type of Functions
The return type of a function can be one of the following:
-
Any type (see [Types]).
-
Functions returning a tuple: The tuple type can be used as the return type of a function, allowing multiple values to be returned as a composite value. In the following example, the return value is a tuple
(a, b)
, and the return type is(Int32, Int32)
.func returnAB(a: Int32, b: Int32): (Int32, Int32) { (a, b) }
-
Function type as the return type: You can use a function type as the return type of another function. In the following example ,the
:
is followed by theadd
function type(Int32, Int32) -> Int32
.func returnAdd(a: Int32, b: Int32): (Int32, Int32) -> Int32 { return {a, b => a + b} // Return a lambda expression. }
To specify the return type of a function, use :Type
after the parameter list of the function's definition. In this case, the type of the function body and the type of e
in all return e
expressions in the function body must be subtypes of the specified type (Type
). Otherwise, a compilation error is reported.
class A {}
class B <: A {}
// Return is not written.
func add(a: Int32, b: Int32): B {
var c = a + b
if (c > 100) {
return B()
} else {
return A() // Compilation error since A is not a subtype of B.
}
}
Specifically, if the return type is specified as Unit (for example, func f(x:Bool):Unit { x}
), the compiler automatically inserts the expression return ()
at all possible return points in the function body so that the return type of the function is always Unit
. The following is an example:
func Column(c: (Data) -> Unit): Unit { 2 } // return () is automatically inserted
func Column(c: (Data) -> Unit) { 2 } // return type is Int64
If the return type of a function is not specified, the compiler infers the return type of the function based on the function body and all return
expressions in the function body. This process is incomplete. For example, if the function is (mutually) recursive and its return type cannot be inferred, a compilation error is reported. (Note: The return type cannot be inferred for a function without a function body.)
The rules for inferring the return type of a function are as follows: The function body is a sequence of expressions and declarations. We denote the type of the last item in the sequence as T0
. (If the last item of a block is an expression, the return type of the function is the type of that expression. If the last item is a declaration, then T0 = Unit
.) Then, we denote the type of e
in all return e
expressions (including those in all subexpressions) in the function body as T1... Tn
. The return type of the function is the minimum common supertype of T0, T1, ..., Tn
. If the minimal common supertype does not exist, a compilation error occurs. The following is an example:
open class Base {}
class Child <: Base {}
func f(a: Rune) {
if (false) {
return Base()
}
return Child()
}
- The type of the function body is the type of the last item in the block, that is, the type of
return Child()
, which isNothing
. - In the first
return e
expressionreturn Base()
, the type ofe
isBase
. - In the second
return e
expressionreturn Child()
, the type ofe
isChild
. - The minimum common supertype of
Nothing
,Base
, andChild
isBase
. Therefore, the return type of this function isBase
.
Note: The return value of a function has the semantics of let
.
Function Declarations
In Cangjie, the difference between function declaration and function definition is that the former does not have a function body. The syntax for function declaration is as follows:
functionDeclaration
: functionModifierList? 'func'
identifier
typeParameters?
functionParameters
(':' type)?
genericConstraints?
;
Function declarations can appear in abstract classes and interfaces.
Function Redefinition
For non-general functions, functions with the same name and parameter type in the same scope are considered redefined, and a compilation error occurs. The following situations should be noted:
- Functions with the same name are considered redefined even if their return types are different.
- A generic function and a non-generic function with the same name never constitutes a redefinition.
- During inheritance, if a function in a subclass has the same name and parameter types as a function in the superclass and its return type is the subtype of the function in the superclass, this constitutes overriding, but not a redefinition. (This is because the scope of the subclass is different from that of the superclass.)
For two generic functions, after one function's generic parameter is renamed, if the non-generalized part of this function and that of the other function constitute a redefinition, the two generic functions constitute a redefinition. An example is as follows:
-
The following two generic functions constitute a redefinition because the renaming of
[T1 |-> T2]
applies to the first function, causing the non-generic parts of both functions to constitute a redefinition.func f<T1>(a: T1) {} func f<T2>(b: T2) {}
-
The following two generic functions do not constitute a redefinition because the previously-mentioned renaming cannot be found.
func f<X,Y>(a:X, b:Y) {} func f<Y,X>(a:X, b:Y) {}
-
The following two generic functions constitute a redefinition because there is a renaming of
[X |-> Y, Y |-> X]
, causing the non-generic parts of both functions to constitute a redefinition.func f<X,Y>(a:X, b:Y) {} func f<Y,X>(a:Y, b:X) {}
Function Types
The function type consists of the parameter type and return type, which are connected using ->
.
functionType:
: '(' (type (, type)*)? ')' '->' type
The following are some examples:
-
Example 1: There is no parameter, and the return type is
Unit
.func hello(): Unit { print("Hello!") } // function type: () -> Unit
-
Example 2: The parameter type is
Int32
, and the return type isUnit
.func display(a: Int32): Unit { print("${a}") } // function type: (Int32) -> Unit
-
Example 3: The two parameters are of the
Int32
type, and the return type isInt32
.func add(a: Int32, b: Int32): Int32 { a + b } // function type: (Int32, Int32) -> Int32
-
Example 4: The parameter type is
(Int32, Int32) -> Int32
,Int32
andInt32
, and the return type isUnit
.func printAdd(add: (Int32, Int32) -> Int32, a: Int32, b: Int32): Unit { print("${add(a, b)}") } // function type: ((Int32, Int32) -> Int32, Int32, Int32) -> Unit
-
Example 5: The two parameters are of the
Int32
type, and the return type is the function type(Int32, Int32) -> Int32
.func returnAdd(a: Int32, b: Int32): (Int32, Int32) -> Int32 { {a, b => a + b} } // function type: (Int32, Int32) -> (Int32, Int32) -> Int32
-
Example 6: If both parameters are of the
Int32
type, the tuple type(Int32, Int32)
is returned.func returnAB(a: Int32, b: Int32): (Int32, Int32) { (a, b) } // function type: (Int32, Int32) -> (Int32, Int32)
Function Calls
For details about the syntax of function call expressions, see [Function Call Expressions].
Named Arguments
When a function is called, the parameter name:
prefix is used before the argument value to specify the parameter corresponding to this argument.
Only parameters defined by !
in the function definition can use the syntax for named arguments.
When a function is called, all named parameters must be passed using named arguments. Otherwise, an error is reported.
In a function call expression, non-named arguments are not allowed after named arguments. When named arguments are used to specify argument values, the sequence of the arguments does not need to match that of the parameter list.
func add(a!: Int32, b!: Int32): Int32 {
a + b
}
var sum1 = add(1, 2) // error
var sum2 = add(a: 1, b: 2) // OK, 3
var sum3 = add(b: 2, a: 1) // OK, 3
Function Call Type Check
This section describes the type check required for a given call expression. If the called function involves overloading, the type check and overloading resolution must be based on the rules of [Function Overloading].
- If a type parameter is specified in the function call expression, the type check can be passed only when the number of type arguments is the same as the number of type parameters. For example, if the function call expression is
f<T1, ..., Tm>(A1, ..., An)
and m type arguments are specified, the number of function type parameters must be m.
open class Base {}
class Sub <: Base {}
func f<X, Y>(a: X, b: Y) {} // f1
func f<X>(a: Base, b: X) {} // f2
f<Base>(Base(), Sub()) // f2 may pass the type checking
- Type check of a function must be based on the arguments in the call expression and the return type
R
specified in the type check context.
Assume that the function is defined as follows:
$$
f_i<T_{i1},...,T_{ip}>(A_{i1}, ..., A_{ik}): R_i \ \ where \ \ C_{i1}, ..., C_{iq_i}
$$
(1) If the call expression contains the type argument, that is, fi<T1, ..., Tp>(A1, ..., Ak)
, the rules for the type check of the fi
function are as follows:
(a) Type argument constraint check: The type argument `<T1, ..., Tp>` must meet the type constraint of the `fi` function.
$$
\sigma = [T_1 \mapsto T_{i1}, ..., T_p \mapsto T_{ip}]
$$
$$
\Delta \vdash \sigma \ \ solves \ \ C_{i1}, ..., C_{iq_i}
$$
(b) Parameter type check: After the type arguments are substituted into the parameters of the function fi
, the argument types (A1, ..., Ak)
are the subtypes of the type after the type argument is substituted into the parameter.
$$
\sigma = [T_1 \mapsto T_{i1}, ..., T_p \mapsto T_{ip}]
$$
$$
\Delta \vdash (A1, ..., Ak) <: \sigma (A_{i1},...,A_{ik})
$$
(c) Return type check: If the context of the call expression requires a specific type R
, type check needs to be performed based on the return type. After the type argument is substituted into the return type Ri
of the function fi
, the return type is the subtype of R
.
$$
\sigma = [T_1 \mapsto T_{i1}, ..., T_p \mapsto T_{ip}]
$$
$$
\Delta \vdash \sigma R_i <: R
$$
(2) If the call expression does not contain the type argument, that is, f(A1, ..., An)
, the rules for the type check of the fi
function are as follows:
(a) If `fi` is a non-generic function, the type check is performed based on the following rules:
(i) Parameter type check: The argument types `(A1, ..., Ak)` are subtypes of the parameter types.
$$
\Delta \vdash (A1, ..., Ak) <: (A_{i1},...,A_{ik})
$$
(ii) Return type check: If the context of the called expression requires a specific type R
, the return type Ri
of the checked function fi
is the subtype of R
.
$$ \Delta \vdash R_i <: R $$
open class Base {}
class Sub <: Base {}
func f(a: Sub) {1} // f1
func f(a: Base) {Base()} // f2
let x: Base = f(Sub()) // f2 can pass the type checking
(b) If `fi` is a generic function, the type check is performed based on the following rules:
(i) Parameter type check: Substitution exists so that the argument types `(A1, ..., Ak)` are the subtypes of the types after parameter type substitution.
$$
\sigma = [T_1 \mapsto T_{i1}, ..., T_p \mapsto T_{ip}]
$$
$$
\Delta \vdash (A1, ..., Ak) <: \sigma (A_{i1},...,A_{ik})
$$
(ii) Return type check: If the context of the call expression requires a specific type R
, type check needs to be performed based on the return type. After the type argument in (i) is substituted into the return type Ri
of the function fi
, the return type is the subtype of R
.
$$ \sigma = [T_1 \mapsto T_{i1}, ..., T_p \mapsto T_{ip}] $$ $$ \Delta \vdash \sigma R_i <: R $$
Note that:
- If a function has a default value, the default value is padded before type check.
- If a function has named parameters, the sequence of the named parameters may be different from that of the parameters. During type check, the named arguments must correspond to the matching named parameters.
Trailing Lambda
When the last parameter of a function is the function type, and the argument corresponding to the function call is lambda
, you can use the trailing lambda syntax to place lambda at the end of the function call outside the parentheses ()
.
func f(a: Int64, fn: (Int64)->Int64) { fn(a) }
f(1, { i => i * i }) // normal function call
f(1) { i => i * i } // trailing lambda
func g(a!: Int64, fn!: (Int64)->Int64) { fn(a) }
g(a: 1, fn: { i => i * i }) // normal function call
g(a: 1) { i => i * i } // trailing lambda
If a function call has only one lambda
as an argument, you can omit ()
and write only lambda
.
func f(fn: (Int64)->Int64) { fn(1) }
f{ i => i * i }
If the trailing lambda does not contain parameters, =>
can be omitted.
func f(fn: ()->Int64) { fn() }
f{ i * i }
Note that the trailing lambda syntax can be used only for function calls with function name/variable name
, and the lambda expression that is trailing lambda interprets it only as the parameter of the function corresponding to function name/variable name
. This means that the following two call examples are invalid:
func f(): (()->Unit)->Unit {
{a => }
}
f() {} // error, the lambda expression is not argument for f.
func g(a: ()->Unit) {}
if (true) { g } else { g } () {} // error, illegal trailing lambda syntax
The preceding expression must be assigned to a variable first. The trailing lambda syntax can be used only when the variable name is used for calling. The code is as follows:
let f2 = f()
f2 {} // ok
let g2 = if (true) { g } else { g }
g2() {} // ok
This syntax can be used for both common function calls and constructor calls, including this()
and super()
.
this(1, { i => i * i } )
this(1) { i => i * i }
super(1, { i => i * i } )
super(1) { i => i * i }
Variable-Length Parameters
Variable-length parameters are special syntactic sugar for function calls. When the last non-named parameter is of the Array
type, the parameter sequence can be directly passed to the corresponding position of the argument to replace the Array
literal.
- Variable-length parameters do not have special declaration syntax. It only requires the last non-named parameter in the function declaration is of the
Array
type. - When a function is called, multiple elements of
Array
can be passed one by one in the form of a common parameter list. - Among non-named parameters, only the last parameter can use variable-length parameters. This syntactic sugar cannot be used for named parameters.
- Variable-length parameters are applicable to global functions, static member functions, instance member functions, local functions, constructors, function variables, lambda, function call operator overloading, and index operator overloading. Other operator overloading, compose, and pipeline calling modes are not supported.
- There may be zero or more variable-length parameters.
- Variable-length parameters take effect only when function overloading does not exist. The priority is the lowest.
func f1(arr: Array<Int64>) {}
func f2(a: Int64, arr: Array<Int64>) {}
func f3(arr: Array<Int64>, a: Int64) {}
func f4(arr1!: Array<Int64>, a!: Int64, arr2!: Array<Int64>) {}
func g() {
let li = [1, 2, 3]
f1(li)
f1(1, 2, 3) // using variable length argument
f1() // using variable length argument
f2(4, li)
f2(4, 1, 2, 3) // using variable length argument
f3(1, 2, 3) // error, Array is not the last parameter
f4(arr1: 1,2,3, a: 2, arr2: 1,2,3) // error, named parameters cannot use variable length argument
}
The function overloading resolution always preferentially considers the functions that can be matched without using variable-length parameters. Variable-length parameters are used for parsing only when no function can be matched.
If the compiler cannot make a resolution, an error is reported.
open class A {
func f(v: Int64): Unit { // f1
}
}
class B <: A {
func f(v: Array<Int64>): Unit { // f2
}
}
func p1() {
let x = B()
x.f(1) // call the f1
}
func g<T>(arg: T): Unit { // g1
}
func g(arg: Array<Int64>): Unit { // g2
}
func p2() {
g(1) // call the g1
}
func h(arg: Any): Unit { // h1
}
func h(arg: Array<Int64>): Unit { // h2
}
func p3() {
h(1) // call the h1
}
Function Scopes
In Cangjie, a function can be defined at the top level of the source program or inside the function.
-
Global Functions
A function defined at the top level of a source program is called a global function, and its scope is global. In the following example, the
globalFunction
function is a global function. Its scope is global.func globalFunction() {}
-
Nested Functions
A function defined in a function body is a nested function, and its scope is local. For details, see [Scopes]. In the following example, the
nestedFunction
function is a nested function, and its scope ranges from its definition to the end of theglobalFunction
function body.func globalFunction() { func nestedFunction() {} }
-
Member Functions
Member functions can be declared or defined in the type definition. The scope of a member function is the entire type and its extensions.
interface Myinterface { func foo(): Unit static func bar(): Unit } class MyClass { func foo() {} static func bar() {} }
-
Member Functions of Extensions
Additional member functions can be declared in an extension. Its scope is all extensions of the extended type and is restricted by access modifiers.
extend MyType { func foo(): Unit {} }
Lambda Expressions
A lambda expression is an anonymous function.
The syntax of the lambda expression is as follows:
lambdaExpression
: '{' NL* lambdaParameters? '=>' NL* expressionOrDeclarations '}'
;
lambdaParameters
: lambdaParameter (',' lambdaParameter)* ','?
;
lambdaParameter
: (identifier | '_') (':' type)?
;
The lambda expression has two forms: one is with parameters, for example, {a: Int64 => e1; e2}
, and the other one is without parameters, for example, { => e1; e2}
. (e1
and e2
are expressions or declaration sequences).
let f1: (Int64, Int64)->Int64 = {a: Int64, b: Int64 => a + b}
var f2: () -> Int32 = { => 123 }
For a lambda expression with parameters, you can use a _
to replace a parameter. A _
indicates the parameters that are not used in the function body of the lambda expression.
let f2 = {n: Int64, _: Int64 => return n * n}
let f3: (Int32, Int32) -> Int32 = {n, _ => return n * n}
let f4: (String) -> String = {_ => return "Hello"}
The lambda expression does not support the declaration of the return type.
-
If the return type of a lambda expression is explicitly specified in the context, its return type is the type specified in the context. If the specified return type is
Unit
, the Cangjie compiler insertsreturn ()
at all possible return points in the function body of the lambda expression so that the return type of the function is alwaysUnit
. The following is an example of specifying the return type asUnit
:func column(c: (Data) -> Unit) {...} func row(r: (Data) -> Unit) {...} func build():Unit { column { _ => row { _ => buildDetail() buildCalendar() } // OK. Well typed since 'return' is inserted. width(750) height(700) backgroundColor("#ff41444b") } // OK. Well typed since 'return' is inserted. }
-
If such context does not exist, the type of the expression on the right of
=>
is considered as the return type of the lambda expression. Similar to functions, if the return type cannot be inferred, a compilation error is reported.
The type annotation of the parameter in the lambda expression can be left empty. The compiler attempts to infer the type from the context. If the compiler cannot infer the type, a compilation error is reported.
var sum1: (Int32, Int32) -> Int32 = {a, b => a + b}
var sum2: (Int32, Int32) -> Int32 = {a: Int32, b => a + b}
var display = { => print("Hello") }
var a = { => return 1 }
The rules for the content on the right of =>
is the same as that of a common function body. You can also omit return
. If the right side of =>
is empty, the return value is ()
.
sum1 = {a, b => a + b}
sum2 = {a, b => return a + b} // Same as that in the previous line.
A lambda expression can be called immediately. The following is an example:
let r1 = {a: Int64, b: Int64 => a + b}(1, 2) // r1 = 3
let r2 = { => 123 }() // r2 = 123
Closures
In Cangjie, a closure is a self-contained function or lambda. A closure can capture variables from the static scope that defines it. Even if a call to a closure is not in the defined scope, the captured variables can still be accessed. Variable capture occurs when a closure is defined.
Not all variable accesses within a closure are considered captures. The following cases are not considered captures:
- A local variable defined inside a function or lambda is accessed in the function or lambda.
- A parameter of a function or lambda is accessed in the function or lambda.
- Global variables and static member variables are accessed in functions or lambdas.
- An instance member variable is accessed in an instance member function. As the instance member function passes
this
as a parameter, all instance member variables are accessed throughthis
in the instance member function.
There are two cases for capturing variables:
- Capturing variables declared by
let
: The values of these variables cannot be modified in closures. If the captured variable is of the reference type, you can change the value of its mutable instance member variable. Functions or lambdas that capture only local variables declared bylet
can be used as first-class citizens. That is, they can be assigned to variables, used as arguments or return values, and used as expressions. - Capturing variables declared by
var
: Functions or lambdas that capture mutable variables can only be called and cannot be used as first-class citizens. For example, they cannot be assigned to variables, used as arguments or return values, or used as expressions.
Note that capturing is transitive. If the
f
function calls theg
function that captures thevar
variable which is not defined inside thef
function, thef
function also captures thevar
variable and cannot be used as a first-class citizen.
In the following example, h
captures only the variable y
declared by let
. h
can be used as a first-class citizen.
func f(){
let y = 2
func h() {
print(y) // OK, captured an immutable variable.
}
let d = h // OK, h can be assigned to variable
return h // OK, h can be a return value
}
In the following example, g
captures the variable x
declared by var
. g
cannot be used as a first-class citizen and can only be called.
func f() {
var x = 1
func g() {
print(x) // OK, captured a mutable variable.
}
let b = g // Error, g cannot be assigned to a variable
g // Error, g cannot be used as an expression
g() // OK, g can be invoked
// Lambda captured a mutable variable, cannot be assigned to a variable
let e = { => print("${x}") } // Error
let i = { => x*x }() // OK, lambda captured a mutable variable, can be invoked.
return g // Error, g cannot be used as a return value.
}
In the following example, g
captures the variable x
declared by var
, f
calls g
, and x
captured by g
is not defined in f
. f
cannot be used as a first-class citizen.
func h(){
var x = 1
func g() { x } // captured a mutable variable
func f() {
g() // invoked g
}
return f // error
}
In the following example, g
captures the x
variable declared by var
, and f
calls g
. However, x
captured by g
is defined inside f
, and f
does not capture other variables declared by var
. Therefore, f
is still used as a first-class citizen.
func h(){
func f() {
var x = 1
func g() { x } // captured a mutable variable
g()
}
return f // ok
}
Functions or lambdas that access global variables, static member variables, and instance member variables modified by var
can still be used as first-class citizens.
class C {
static var a: Int32 = 0
static func foo() {
a++ // OK
return a
}
}
var globalV1 = 0
func countGlobalV1() {
globalV1++
C.a = 99
let g = C.foo // OK
}
func g(){
let f = countGlobalV1 // OK
f()
}
The captured variables must meet the following rules:
- A captured variable must be visible when a closure is defined. Otherwise, an error is reported during compilation.
- Variables must have been initialized before being captured. Otherwise, an error is reported during compilation.
- If a function contains a local variable with the same name as a variable outside the function, when the closure in the function captures the variable outside the function because the scope of the local variable does not start, a warning is reported to avoid misuse.
// 1. The captured variable must be defined before the closure.
let x = 4
func f() {
print("${x}") // Print 4.
let x = 99
func f1() {
print("${x}")
}
let f2 = { =>
print("${x}")
}
f1() // Print 99.
f2() // Print 99.
}
// 2. The variable must be initialized before being captured.
let x = 4
func f() {
print("${x}") // Print 4.
let x: Int64
func f1() {
print("${x}") // Error: x is not initialized yet.
}
x = 99
f1()
}
// 3. If there is a local variable in a block, closures capture variables of the same name in the outer scope will report a warning.
let x = 4
func f() {
print("${x}") // Print 4.
func f1() {
print("${x}") // warning
}
let f2 = { =>
print("${x}") // warning
}
let x = 99
f1() // print 4
f2() // print 4
}
Function Overloading
Definition of Function Overloading
In Cangjie, function overloading occurs when multiple function definitions with the same function name exist in the same scope, but they do not constitute redefinition. When function overloading exists, the correct function definition to be used is determined during the function call based on the types of the arguments in the call expression and the context.
Rules for Function Overloading
-
The function name must be the same and meet one of the following conditions:
- The function name is introduced by the
func
keyword. - The function is a constructor defined in a class or struct, including the main constructor and the
init
constructor.
- The function name is introduced by the
-
The parameter types must be different. That is, one of the following conditions must be met:
-
The number of function parameters is different.
-
The number of function parameters is the same, but the parameter types in the corresponding positions are different.
-
-
The functions must be visible in the same scope.
Note that the parameter type of a function is not affected by whether the parameter is a named parameter or whether the parameter has a default value. In addition, when the parameter type is discussed, a type and its alias are considered to be the same type. For example, the parameter types of the four functions in the following example are the same:
type Boolean = Bool
func f(a : Bool) {}
func f(a! : Bool) {}
func f(a! : Bool = false) {}
func f(a! : Boolean) {}
Example 1: The two functions defined at the top level of the source file have different parameter types. f
constitutes overloading.
// f overloading
func f() {...}
func f(a: Int32) {...}
Example 2: The f1
function in the I
interface, C1
class, and C2
class constitutes overloading.
interface I {
func f1() {...}
}
open class C1 {
func f1(a: Int32) {...}
}
class C2 <: C1 & I {
// f1 overloading
func f1(a: Int32, b: String) {...}
}
Example 3: Constructors of a type constitute overloading.
class C {
var name: String = "abc"
// constructor overloading
init(){
print(name)
}
init(name: String){
this.name = name
}
}
Example 4: In the following example, the number of function parameters is the same, the types at corresponding positions are the same, and only the constraints of the type variables contained in the parameter types are different. This does not constitute overloading.
interface I1{}
interface I2{}
func f<T>(a: T) where T <: I1 {}
func f<T>(a: T) where T <: I2 {} // Error, not overloading
mut Functions
The mut
function is a special instance member function. In the mut
member function of the struct type, member variables can be modified through this
. In the non-mut
member function of the struct type, member variables cannot be modified through this
.
Definition of the mut Function
The mut
function is modified by the mut
keyword and can be defined only in the interface, struct, and struct extensions. In addition, it can be used only for instance member functions (static member functions are not supported).
struct A {
mut func f(): Unit {} // ok
mut static func g(): Unit {} // error
mut operator func +(rhs: A): A { // ok
return A()
}
}
extend A {
mut func h(): Unit {} // ok
}
class B {
mut func f(): Unit {} // error
}
interface I {
mut func f(): Unit // ok
}
In the mut
function, you can assign values to the fields of the struct instance. The values will modify the instance and take effect immediately. Like instance member functions, this
is not required and can be inferred by the compiler.
struct Foo {
var i = 0
mut func f() {
this.i += 1 // ok
i += 1 // ok
}
}
main() {
var a = Foo()
print(a.i) // 0
a.f()
print(a.i) // 2
a.f()
print(a.i) // 4
return 0
}
this
in the mut
function has the following restrictions:
- It cannot be captured (meaning that the fields of the current instance cannot be captured).
- It cannot be used as an expression.
struct Foo {
var i = 0
mut func f(): Foo {
let f1 = { => this } // error
let f2 = { => this.i = 2 } // error
let f3 = { => this.i } // error
let f4 = { => i } // error
return this // error
}
}
mut Function in Interfaces
The struct type must be modified by the same mut
modifier when implementing an interface function. When a type other than struct implements an interface function, the mut
modifier cannot be used.
interface I {
mut func f1(): Unit
func f2(): Unit
}
struct A <: I {
public mut func f1(): Unit {} // ok
public func f2(): Unit {} // ok
}
struct B <: I {
public func f1(): Unit {} // error
public mut func f2(): Unit {} // error
}
class C <: I {
public func f1(): Unit {} // ok
public func f2(): Unit {} // ok
}
Note that when a struct instance is assigned to an interface type, the copy semantics is used. Therefore, the mut function of the interface cannot change the value of the struct instance.
interface I {
mut func f(): Unit
}
struct Foo <: I {
var v = 0
public mut func f(): Unit {
v += 1
}
}
main() {
var a = Foo()
var b: I = a
b.f()
print(a.v) // 0
return 0
}
Access Rules
If a variable is declared by let
and the type may be struct (including the static type being struct, or the type variable might be of the struct type), the variable cannot access the function that is modified by mut
. In other cases, the access is allowed.
interface I {
mut func f(): Unit
}
struct Foo <: I {
var i = 0
public mut func f(): Unit {
i += 1
}
}
class Bar <: I {
var i = 0
public func f(): Unit {
i += 1
}
}
main() {
let a = Foo()
a.f() // error
var b = Foo()
b.f() // ok
let c: I = Foo()
c.f() // ok
return 0
}
func g1<T>(v: T): Unit where T <: I {
v.f() // error
}
func g2<T>(v: T): Unit where T <: Bar & I {
v.f() // ok
}
If the type of a variable may be struct (including the static type being struct, or the type variable might be of the struct type), then the variable cannot use functions modified by mut
of this type as high-order functions. It can only call those mut
functions.
interface I {
mut func f(): Unit
}
struct Foo <: I {
var i = 0
public mut func f(): Unit {
i += 1
}
}
class Bar <: I {
var i = 0
public func f(): Unit {
i += 1
}
}
main() {
var a = Foo()
var fn = a.f // error
var b: I = Foo()
fn = b.f // ok
return 0
}
func g1<T>(v: T): Unit where T <: I {
let fn = v.f // error
}
func g2<T>(v: T): Unit where T <: Bar & I {
let fn = v.f // ok
}
Non-mut
instance member functions (including lambda expressions) cannot access the mut
function of this
, while the reverse is allowed.
struct Foo {
var i = 0
mut func f(): Unit {
i += 1
g() // ok
}
func g(): Unit {
f() // error
}
}
interface I {
mut func f(): Unit {
g() // ok
}
func g(): Unit {
f() // error
}
}