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 by override 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 the add 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 is Nothing.
  • In the first return e expression return Base(), the type of e is Base.
  • In the second return e expression return Child(), the type of e is Child.
  • The minimum common supertype of Nothing, Base, and Child is Base. Therefore, the return type of this function is Base.

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:

  1. 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) {}
    
  2. 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) {}
    
  3. 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 is Unit.

    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 is Int32.

    func add(a: Int32, b: Int32): Int32 { a + b }
    
    // function type: (Int32, Int32) -> Int32
    
  • Example 4: The parameter type is (Int32, Int32) -> Int32, Int32 and Int32, and the return type is Unit.

    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].

  1. 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
  1. 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:

  1. If a function has a default value, the default value is padded before type check.
  2. 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.

  1. 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.
  2. When a function is called, multiple elements of Array can be passed one by one in the form of a common parameter list.
  3. Among non-named parameters, only the last parameter can use variable-length parameters. This syntactic sugar cannot be used for named parameters.
  4. 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.
  5. There may be zero or more variable-length parameters.
  6. 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 the globalFunction 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 inserts return () at all possible return points in the function body of the lambda expression so that the return type of the function is always Unit. The following is an example of specifying the return type as Unit:

    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 through this 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 by let 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 the g function that captures the var variable which is not defined inside the f function, the f function also captures the var 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 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
    }
}