Types

The Cangjie programming language is a statically-typed language. Most type checks that ensure program security occur during compilation. In addition, the Cangjie programming language is a strongly-typed language. Each expression has a type, and the type of an expression determines its value space and supported operations. Statically-typed and strongly-typed mechanisms can help programmers locate a large number of program errors caused by types during compilation.

Types in Cangjie may be classified into the immutable types and mutable types. Immutable types include the numeric (which can be an integer or a floating-point number, see [Numeric]), Rune, Bool, Unit, Nothing, String, Tuple, Range, Function, and enum. Mutable types include Array, VArray, struct, class, and interface.

The difference between the immutable type and the mutable type is that the data value of the immutable type does not change after initialization, and the data value of the mutable type can be changed after initialization.

The following describes each type, values of types, and operations supported by types.

Immutable Types

The following sequentially describes immutable types in the Cangjie programming language.

Numeric

Numeric types include integer and floating-point number types, which are respectively used to represent integers and floating-point numbers. Integer types include signed integer and unsigned integer types. Signed integer types include Int8, Int16, Int32, Int64, and IntNative, indicating signed integers whose encoding length are 8 bits, 16 bits, 32 bits, 64 bits, and platform-dependent sizes, respectively. Unsigned integer types include UInt8, UInt16, UInt32, UInt64, and UIntNative, indicating unsigned integers whose encoding length are 8 bits, 16 bits, 32 bits, 64 bits, and platform-related sizes, respectively. Floating-point number types include Float16, Float32, and Float64, indicating floating-point numbers whose encoding lengths are 16 bits, 32 bits, and 64 bits, respectively.

The lengths of IntNative and UIntNative are the same as the bit width of the current system.

The following table lists all numeric types and their ranges.

TypeRange
Int8$-2^7 \sim 2^7-1$ (-128 to 127)
Int16$-2^{15} \sim 2^{15}-1$ (-32768 to 32767)
Int32$-2^{31} \sim 2^{31}-1$ (-2147483648 to 2147483647)
Int64$-2^{63} \sim 2^{63}-1$ (-9223372036854775808 to 9223372036854775807)
IntNativeplatform dependent
UInt8$0 \sim 2^8-1$ (0 to 255)
UInt16$0 \sim 2^{16}-1$ (0 to 65535)
UInt32$0 \sim 2^{32}-1$ (0 to 4294967295)
UInt64$0 \sim 2^{64}-1$ (0 to 18446744073709551615)
UIntNativeplatform dependent
Float16See IEEE 754 binary16 format
Float32See IEEE 754 binary32 format
Float64See IEEE 754 binary64 format

For ease of use, the Cangjie programming language specifically provides some type aliases (for details about the type aliases, refer to the type alias section below):

  1. Byte is used as the type alias of UInt8. The Byte type is equivalent to the UInt8 type.
  2. Int and UInt are used as the aliases of the Int64 and UInt64 types, respectively. The Int type is equivalent to the Int64 type, and the UInt type is equivalent to the UInt64 type.
let a: UInt8 = 128
let b: Byte = a // ok

let c: Int64 = 9223372036854775807
let d: Int = c // ok

let e: UInt64 = 18446744073709551615
let f: UInt = e // ok

The following describes numeric literals and operations supported by the numeric type.

Numeric Literals

Values such as 5, 24, 2.9, and 3.14 are numeric literals. Literals can only be called by their values, and cannot be changed.

The following describes integer literals and floating-point literals:

Integer literals can be expressed in four forms: binary, octal, decimal, and hexadecimal. The binary value starts with 0b (or 0B), the octal value starts with 0o (or 0O), the hexadecimal value starts with 0x (or 0X), and the decimal value has no prefix. For example, the decimal value 24 may be defined as any one of the following four forms, where the underlined _ is used as a separator to facilitate identification of a quantity of digits of the value.

0b0001_1000     // Binary.
0o30            // Octal.
24              // Decimal.
0x18            // Hexadecimal.

For the syntax definition of integer literals, see [Integer Literals].

Floating-point types comply with the IEEE 754 format. Float16 uses the half-precision format (binary16) of IEEE 754, Float32 uses the single-precision format (binary32) of IEEE 754, and Float64 uses the double-precision format (binary64) of IEEE 754. Generally, a floating-point number contains the integer part, decimal part (including the decimal point), and exponent part. It can be notated in decimal or hexadecimal format. In the decimal format, a floating-point number must contain an integer or a decimal. If there is no decimal, the floating-point literal must contain the exponent part (with e or E as the prefix and 10 as the base number). In the hexadecimal format, a floating-point number must contain an integer or a decimal (with 0x as the prefix) and the exponent part (with p or P as the prefix and 2 as the base number). Pay attention to the following special values in floating-point parameters: positive infinity (POSITIVE_INFINITY), negative infinity (NEGATIVE_INFINITY), Not a Number (NaN), positive 0 (+0.0), and negative 0 (-0.0). Infinity indicates that an operation result exceeds a representation range. For example, when two large floating-point numbers are multiplied, or a non-zero floating-point number is divided by a floating-point zero, the result is infinity, and a sign of infinity is determined by a sign of an operand, complying with the following rule: A positive and a negative make a negative, and two negatives make a positive. NaN indicates that the value is neither a real number nor an infinity. For example, the result of multiplying positive infinity by 0 is NaN. The following is an example of a floating-point literal:

3.14 		// decimal Float64 3.14.
2.4e-1 		// decimal Float64 0.24.
2e3 		// decimal Float64 2000.0.
.8 			// decimal Float64 0.8.
.123e2 		// decimal Float64 12.3.
0x1.1p0 	// hexadecimal Float64 1.0625 (decimal value).
0x1p2 		// hexadecimal Float64 4 (decimal value).
0x.2p4 		// hexadecimal Float64 2 (decimal value).

For the syntax definition of floating-point literals, see [Floating-Point Literals].

A minimum positive value of a floating-point decimal type in the Cangjie programming language is a subnormal floating-point number.

For the Float32 type, the minimum positive floating-point number is $2^{-149}$, which is about 1.4e-45. The maximum positive floating-point number is $(2 - 2^{-23}) \times 2^{127}$, which is about 3.40282e38.

For the Float64 type, the minimum positive floating-point number is $2^{-1074}$, which is about 4.9e-324. The maximum positive floating-point number is $(2 - 2^{-52}) \times 2^{1023}$, which is about 1.79769e308.

If the face value of a floating-point decimal other than $0$ is rounded to $0$ or infinity because it is too small or too large, the compiler generates an alarm.

Operators Supported by Numeric Types

Numeric types support the following operators: arithmetic operators, bitwise operators, relational operators, increment and decrement operators, unary minus operators, and (compound) assignment operators. The integer type supports all the preceding operators, and the floating-point type supports all operators except bitwise operators. Therefore, the following briefly describes these operators of the integer type. For details, see [Expressions].

Arithmetic operators include addition (+), subtraction (-), multiplication (*), division (/), modulo (%), and power (**). By default, if arithmetic operators are not overloaded (see [Operator Overloading] for details about how to overload operators), the two operands of arithmetic operators must be of the same type. To operate two operands of different types, forcibly convert the type first. For details about the priority and binding of arithmetic operators, see [Arithmetic Expressions]. The following example describes an arithmetic operation on an integer type:

2 + 3                        // add result: 5
3 - 1                        // sub result: 2
3 * 9                        // mul result: 27
24 / 8                       // div result: 3
7 % 3                        // mod result: 1
2 ** 3                       // power result: 8
5 + 15 - 2 * 10 / 4          // result: 15
5 + 10 - 3 * 4 ** 2 / 3 % 5  // result: 14

Bitwise operators include bitwise NOT (!), left shift (<<), right shift (>>), bitwise AND (&), bitwise XOR (^), and bitwise OR (|). For details about bitwise operators, see [Bitwise Operation Expressions]. The following example describes a bit operation on an integer type:

!10         // bitwise logical NOT operator: -11
10 << 1     // left shift operator: 10*2=20
10 >> 1     // right shift operator: 10/2=5
10 & 15     // bitwise logical AND operator: 10
10 ^ 15     // bitwise XOR operator: 5
10 | 15     // bitwise logical OR operator: 15
0b1010 & 0b1110 ^ 0b1111 | 0b0101   // result: 0b0101

Relational operators include less than (<), greater than (>), less than or equal to (<=), greater than or equal to (>=), equal to (==), and not equal to (!=). The result of a relational expression is a value of the Bool type (true or false). For details about relational operators, see [Relational Expressions]. The following example describes a relational operation on an integer type:

2 == 3  	// result: false
2 != 3  	// result: true
8 < 10  	// result: true
9 <= 9  	// result: true
24 > 28  	// result: false
24 >= 23  	// result: true

Increment and decrement operators include increment (++) and decrement (--), which can be considered as special assignment operators (see below) to increase or decrease the value of a variable by 1. Increment and decrement operators can be used only as a suffix operator and can be used only for mutable variables of the integer type, because the values of immutable variables and integer literals cannot be modified. For details about the increment and decrement operators, see [Increment and Decrement Expressions]. The following example describes an increment or decrement operation on an integer type:

main() {
    var i: Int32 = 5
    var j: Int32 = 6
    i++ // i=6
    j-- // j=5   
    return 0
}

A unary minus operator is represented by -, and the result is the negative of its operand. For details about -, see the description of [Arithmetic Expressions]. The following example describes a unary minus operation on an integer type:

var i: Int32 = 5
var j: Int64 = 100 
var k: Int32 = -i   // k = -5
var l: Int64 = -j   // l = -100 

(Compound) assignment operators include assignment (=) and compound assignment (op=). op can be any binary operator among arithmetic operators, logical operators, and bitwise operators. The (compound) assignment operation can assign a value of an expression of the numeric type to a variable of the numeric type. For details about (compound) assignment operators, see [Assignment Expressions]. The following example describes a (compound) assignment on an integer type:

main() {
    var x: Int64 = 5
    var y: Int64 = 10
    x = y 	// assignment: x = 10
    x += y 	// compound assignment: x = 20
    x -= y 	// x = 10
    x *= y 	// x = 100
    x /= y 	// x = 10
    x %= y 	// x = 0
    x = 5 	// x = 5
    x **= 2 // x = 25
    x <<= 1 // x = 50
    x >>= 2 // x = 12
    x &= y 	// x = 8
    x ^= y 	// x = 2
    x |= y 	// x = 10
    return 0
}

Operators Supported by Floating-Point Numbers

Floating-point types support the following operations (except bit operations): arithmetic operations, relational operations, increment and decrement operations, unary positive (negative) operations, conditional operations, and (composite) assignment operations. The calculation of floating-point numbers may use operations with different precisions from their own types. Note the following special cases in floating-point operations: In a relational expression, if the value of an operand is NaN, the result of the expression is false, except that the results of NaN != x and x != NaN are true (x can be any floating-point number including NaN).

let x = NaN
var y = 3.14
var z = x < y  // z = false
var v = x != x // v = true
var w = (x < y) == !(x >= y) // w = false

In arithmetic expressions, values containing NaN or infinite expressions comply with the IEEE 754 standard.

Rune

The Cangjie programming language uses the Rune type to represent Unicode code point. These values can be classified into printable characters, non-printable characters (characters that cannot be displayed, such as control characters), and special characters (such as single quotation marks and backslashes).

  • Rune literals

    For the syntax definition of Rune literals, see [Rune Literals].

A Rune literal consists of a pair of single quotation marks and characters, for example:

    'S'
    '5'
    ' ' // blank

Non-printable and special character literals are defined using escape characters (\):

Escape CharacterCharacter
\0Empty character
\\backslash \
\bBackspace
\fWriter
\nNew line
\rEnter
\tHorizontal tab
\vVertical tab
\'Single quotation mark '
\"Double quotation marks "
\u{X}Any Unicode code point, where X is a 1-8 digit hexadecimal number
  • Operations supported by the Rune type

    The Rune type supports only relational operators (compared based on the Unicode code point).

    main() {
        'A'=='A' 	// result: true
        'A'!='A' 	// result: false
        'A'<'a' 	// result: true
        'A'<='A' 	// result: true
        'A'>'a' 	// result: false
        'A'>='A' 	// result: true
        return 0
    }
    

Boolean

The Cangjie programming language uses Bool to indicate the Boolean type.

  • Bool literal

    The Bool type has only two literals: true and false.

    let bool1: Bool = true
    let bool2: Bool = false
    
  • Operations supported by the Bool type

    The Cangjie programming language does not support type conversion between the numeric type and Bool. Non-Bool values cannot be used as Bool values. Otherwise, a compilation error is reported. The Bool type supports the following operations: assignment, partial compound assignment (&&= and ||=), partial relation (== and !=), and all logical operations (!, &&, and ||). The following is an example:

    main() {
        let bool1: Bool = true
        var bool2: Bool = false
        bool2 = true    // assignment
        bool2 &&= bool1 // bool2=true
        bool2 ||= bool1 // bool2=true
        true == false   // return false
        true != false   // return true
        !false          // logical NOT, return true
        true && false   // logical AND, return false
        false || false  // logical OR, return false
        return 0
    }
    

Unit

The Unit type has only one value: ().

Nothing

Nothing is a special type that does not contain any value, and Nothing is a subtype of all types. Nothing indicates dead code. For example, if a variable is of the Nothing type, it will never be used, and no memory needs to be created for it. If a parameter of a function call is of the Nothing type, the subsequent parameters will not be evaluated, and the function call itself will not be executed. If an expression in a block is of the Nothing type, all expressions and declarations following the expression will not be executed. The Cangjie compiler generates a compilation alarm for the detected dead code.

For details about which expressions have the Nothing type, see [Control Transfer Expressions].

String

The Cangjie programming language uses String to indicate the string type, which is used to express text data and consists of a string of Unicode characters.

String Literals

String literals are classified into three types: single-line string literals, multi-line string literals, and multi-line original string literals.

For details about the syntax definition of string literals, see [String Type Literals].

Single-line string literals are defined using a pair of double quotation marks (for example, "singleLineStringLiteral"). The content in the double quotation marks can be any number of characters (except non-escaped double quotation marks and a separate \). A single-line string literal can be written in only one line.

let s1 = "" 	// empty string
let s2: String = "Hello Cangjie Lang" 		// define string s2 
var s3 = "\"Hello Cangjie Lang\"" 			// define string s3 containing character "
var s4: String = "Hello Cangjie Lang\n"	// define string s4 containing character \n
// illegal string with character \
let s5 = "hello\"
// illegal string with character "
let s6 = "Hello "Cangjie Lang"

A multi-line string literal starts and ends with three double quotation marks ("""). A new line is required after the double quotation marks at the beginning. Otherwise, an error is reported. The literal starts from the first line after the double quotation marks at the beginning and ends with the first three non-escaped double quotation marks. The content between the literals can be any number of valid characters (except the three non-escaped double quotation marks """ and the separate \). Before the current file ends, if the three non-escaped double quotation marks are not encountered, a compilation error is reported. Unlike single-line string literals, multi-line string literals can span multiple lines.

// empty multi-line string
let s1 = """
"""

// Error: there must be a line terminator after the beginning double quotations. 
let errorStr = """abc
""" 

/*
The result of s2 is:
This
    is a multi-line string
*/
let s2 = """
This
    is a multi-line string"""
    
/*
The result of s3 is:
    This
  is a multi-line string
*/
let s3 = """
    This
  is a multi-line string"""
  
/*
The result of s4 is:
This
  is a 
multi-line string
*/
let s4 = """
This
    is a 
multi-line string
"""

/*
The result of s5 is:
This is a 

    multi-line string
*/
let s5 = """
This is a\n
    multi-line string
"""

Multi-line original string literal starts with one or more comment tags (#) and a double quotation mark ("), and ends with a double quotation mark (") and the number of comment tags (#) at the beginning. Note that the matching ends when the string literal value encounters the first matched double quotation mark (") and the number of comment tags (#) at the beginning. The content between the start and end double quotation marks (") can be any number of valid characters (except a double quotation mark and the same number of comment tags (#) at the beginning). Before the current file ends, if no matching quotation mark and the same number of comment tags are encountered, a compilation error is reported. Unlike (common) multi-line string literals, the content in the original multi-line string literal remains unchanged (escape characters are not escaped).

// empty multi-line raw string
let empty1 = #""#
// empty multi-line raw string
let empty2 = ##""##
 
/*
The result of s2 is:

    This
  is a multi-line string
*/
let s2 = ##"
    This
  is a multi-line raw string"##

/*
The result of s3 is:
This is a\n 
    multi-line string
*/
let s3 = #"This is a\n
    multi-line string"#

/*
The result of s4 is:
 This is a "#
*/ 
let s4 = ##" This is a "#
"##
// Error: the beginning and ending numbers of `#` are not matched.
let errorStr1 = ##" error string "#
// Error: the beginning and ending numbers of `#` are not matched.
let errorStr2 = ##" error string "###
// Error: the string literal is terminated when meeting the first matched `"##`.
let errorStr3 = ##" error string "## error "##

The String type can be determined to equality using == (inequality using !=). Two character strings are the same only when their corresponding code point sequences are the same.

Interpolated String

An interpolated string is a string literal that contains one or more interpolation expressions (not applicable to multi-line raw string literals). When an interpolation string is parsed into a result string, the position of each interpolation expression is replaced with the result of the corresponding expression.

Each interpolation expression must be enclosed in {} parentheses and prefixed with $. The expression and declaration sequence in braces can be the same as that in [Block], but cannot be empty. Interpolation expressions in multi-line strings support line breaks.

let obj = "apples"
let count = 10
let interps1 = "There are ${count * count} ${obj}."
// The result of "interps1" is: There are 100 apples.
let error = "Empty sequence is not allowed ${}" // Error

If the $ symbol in the character string is not followed by the { symbol, the character string is processed as a common $ symbol. If the escape character \ is added before $, no matter whether $ is followed by {, $ is not processed as an interpolation expression.

let d1 = "The $ sign."
// The result of "d1" is: The $ sign.
let d2 = "The \${v}."
// The result of "d2" is: The ${v}.

Interpolation expressions also support user-defined types as long as the types meet the ToString interface restrictions. The preceding types meet the ToString interface requirement.

Tuple

In the Cangjie programming language, a Tuple type is an immutable type formed by combining a plurality of different types, and is represented by (Type1, Type2, ..., TypeN), where N indicates a dimension of the tuple. The tuple types are described as follows:

  1. Type1 to TypeN can be of any type (N must be greater than or equal to 2, that is, Tuple must be at least binary).

  2. For each dimension of a tuple, for example, the Kth dimension, an instance of any TypeK subtype may be stored.

  3. When Type1 to TypeN in (Type1, Type2, ..., TypeN) use == for value equality (!= for value inequality), the Tuple type supports == for value equality (!= is used for value inequality). Otherwise, the Tuple type does not support == and !=. In this case, if == and != are used, an error is reported during compilation. Two Tuple instances of the same type are equal only when all elements in the same position (index) are equal (indicating that their lengths are equal).

The syntax of the Tuple type is defined as follows:

tupleType
    : '(' type (',' type)+ ')'
    ;

Tuple Literals

A tuple literal is expressed in the format of (expr1, expr2, ..., exprN). Multiple expressions are separated by commas (,), and each expression can have a different type. The syntax of the Tuple literal is defined as follows:

tupleLiteral
    : '(' expression (',' expression)+ ')'
    ;

Example of tuple literals:

(3.14, "PIE") 
(2.4, 3.5) 		
(2, 3, 4) 		
((2, 3), 4)

Deconstructing a Tuple

A tuple can also be used to deconstruct another tuple value. Elements at different positions in a tuple are bound to different variables. The following example shows how to deconstruct the return value of a multi-return-value function:

func multiValues(a: Int32, b: Int32): (Int32, Int32) { 
    return (a + b, a - b) // The type of the return value of the function multiValues is (Int32, Int32).
}

main() {
    var (x, y) = multiValues(8,24) // Define an anonymous tuple who has two elements, i.e., x and y.
    print("${x}") // output: 32
    print("${y}") // output: -16
    return 0
}

Access Through a Tuple Index

A tuple allows you to access an element in a specific position using tupleName[index]. (index starts from 0 and can only be a literal of the Int64 type.)

main() {
    var z = multiValues(8, 24) // the type of z is inferred to be (Int32, Int32)
    print("${z[0]}") // output: 32
    print("${z[1]}") // output: -16
    return 0
}

Defining a Variable of the Tuple Type

When defining a variable of the tuple type, you can omit the type annotation. The compiler infers the specific type based on the program context.

let tuplePIE = (3.14, "PIE")     // The type of tuplePIE is inferred to be (Float64, String).
var pointOne = (2.4, 3.5) 		// The type of pointOne is inferred to be (Float64, Float64).
var pointTwo = (2, 3, 4) 		// The type of pointTwo is inferred to be (Int64, Int64, Int64).
var pointThree = ((2, 3), 4) 	// The type of pointThree is inferred to be ((Int64, Int64), Int64).

Range

The Cangjie programming language uses Range<T> to indicate the Range type, and requires that T can be instantiated only to implement the Comparable interface and Countable interface types. The Range type is used to represent a sequence with a fixed step and is an immutable type.

Each Range<T> instance contains the following values: start, end, and step. start and end indicate a start value and an end value of the sequence, respectively. step indicates a difference between two adjacent elements in the sequence.

The Range type can be determined to equality using == (inequality using !=). Two Range instances of the same type are the same only when they are both left-closed and right-open or left-closed and right-closed, the values of start, end, and step are the same.

Creating a Range Instance

There are two Range operators: .. and ..=, which are used to create left-closed and right-open Range instances and left-closed and right-closed Range instances, respectively. They are used in start..end:step and start..=end:step, in which start, end, and step are expressions. The two expressions are described as follows:

  1. The types of start and end must be the same, and the type of step must be Int64.

  2. In the expression start..end:step, when step > 0 and start >= end or step < 0 and start <= end, start..end:step returns an empty Range instance (an empty sequence that does not contain any element). If the result of start..end:step is a non-empty Range instance, the number of elements in the Range instance is equal to the round-up value of (end-start)/step (that is, greater than or equal to the minimum integer of (end-start)/step).

  3. In the expression start..=end:step, when step > 0 and start > end or step < 0 and start < end, start..=end:step returns an empty Range instance. If the result of start..=end:step is a non-empty Range instance, the number of elements in the Range instance is equal to the round-down value of ((end-start)/step)+1 (that is, less than or equal to the maximum integer of ((end-start)/step)+1).

let range1 = 0..10:1 	// Define a half-open range [0,10) (with step = 1) which contains 0, 1, 2, 3, 4, 5, 6, 7, 8 and 9.
let range2 = 0..=10:2 	// Define a closed range [0,10] (with step = 2) which contains 0, 2, 4, 6, 8 and 10.
let range3 = 10..0:-2 	// Define a half-open range [10,0) (with step = -2) which contains 10, 8, 6, 4 and 2.
let range4 = 10..=0:-1 	// Define a closed range [10,0] (with step = -1) which contains 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 and 0.
let range5 = 10..0:1 	// Define an empty range.
let range6 = 0..0:1 	// Define an empty range.
let range7 = 0..=0:1    // Define a closed range [0,0] (with step = 1) which only contains 0.
let range8 = 0..=10:-1 	// Define an empty range.
  1. In the start..end:step and start..=end:step expressions, the value of step cannot be 0.
let range9 = 0..10: 0 				// error: the value of the step should not be zero.
let range10 = 0..=10: 0 			// error: the value of the step should not be zero.
  1. When the Range<Int64> instance is used in the index operator [], start or end can be omitted. In other scenarios, start and end are required, and only step is optional (step=1 is used by default).
let range11 = 1..10 		 // Define a half-open range [1, 10) with step = 1.
let range12 = 1..=10 		 // Define a closed range [1, 10] with step = 1.	
let range13 = ..10 		     // error: the start value is required.
let range14 = 1.. 		     // error: the end value is required.
let range15 = .. 		     // error: the start and end value are required.

Using a Range Type

Expressions of the Range type are mainly used in for-in expressions (see [Loop Expressions]) and are used as indexes to truncate segments. Note that:

  • When an instance of the Range<Int64> type is used in the index operator [], both start and end are optional. Their values are determined by the context. If the index operator [] is overloaded on a custom type and the parameter type is Range<Int64>, the value of start is 0 when start is omitted, and the value of end is equal to the maximum value of Int64 when end is omitted.

Defining a Variable of the Range Type

When defining a variable of the Range<T> type, you can explicitly add a type annotation or omit the type annotation (in this case, the compiler infers the type annotation based on the program context).

let range16: Range<Int64> = 0..10 		// Define a half-open range [0,10) with step = 1
let range17: Range<Int64> = -10..10:2 	// Define a half-open range [-10,10) with step = 2
let range18 = 0..=10 		            // Define a closed range [0,10] with step = 1
let range19 = -10..=10:2 	            // Define a closed range [-10,10] with step = 2

Function

A function type represents a type of a specific function, and is immutable. The syntax of the function type is defined as follows:

arrowType 
    : parameterTypes '->' type
    ;
parameterTypes
    : '(' (type (',' type)*)? ')'
    ;

parameterTypes (parameter type list) and type (return type) are connected using ->. A pair of () in the parameter type list is required. () can contain zero or more types (multiple types are separated by ,).

() -> Int32 	// A function type has an empty parameter type list, and its return value type is 'Int32'.
() -> Unit 		// A function type has an empty parameter type list, and its return value type is 'Unit'.
(Float32) -> Int32 	// A function type has one parameter whose type is 'Float32', and its return value type is 'Int32'
(Int32, Int32, Float32) -> Int32 // A function type has three parameters, and its return value type is 'Int32'
(Int32, Int32, Float32) -> (Int32, Int32) // A function type has three parameters, and its return value type is '(Int32, Int32)'	
(Int32, Int32, Float32) -> Unit // A function type has three parameters, and its return value type is 'Unit'.		

In the Cangjie programming language, a function is a first-class citizen, which indicates that the function can be used as a parameter of another function, can be used as a return value of another function, or can be directly assigned to a variable.

The function type does not support == or != for equality or inequality, respectively. For more information about functions, see [Functions].

Enum

The enum type is an immutable type used to organize a set of related values (which are called constructor). A value of the enum type can be obtained only from a constructor defined in the enum type at any time.

Defining the Enum Type

The syntax for defining the enum type is as follows:

enumDefinition
    : enumModifier? 'enum' identifier typeParameters? ('<:' superInterfaces)? genericConstraints? '{' enumBody '}'
    ;
    
enumBody
    : '|'? caseBody ('|' caseBody)* (functionDefinition | operatorFunctionDefinition | propertyDefinition | macroExpression)*
    ;
    
caseBody
    : identifier ('(' type (',' type)* ')')?
    ;

enumModifier is the modifier (that is, public) of the enum type, enum is the keyword, identifier is the name of the enum type, and typeParameters and genericConstraints are the type variable list and type variable constraints, respectively. For details, see [Generics]. Multiple values of caseBody (constructors) can be defined in enumBody. Each constructor can have no parameter or a group of parameters of different types. Multiple constructors are separated by |. Before the first constructor, | can exist, which is optional. You can define other members of the enum after caseBody, including member functions, operator overloading functions, and member attributes.

The following is an example of the enum type:

/*
Define an enum type 'TimeUnit1' who has four constructors: 'Year', 'Month', 'Day' and 'Hour'.
*/
enum TimeUnit1 {
    Year | Month | Day | Hour
}

/* 
'TimeUnit2' has four constructors: 'Year(Int32)', 'Month(Int32, Float32)', 'Day(Int32, Float32, Float32)' and 'Hour(Int32, Float32, Float32, Float32)'.
*/
enum TimeUnit2 { 
    | Year(Float32)
    | Month(Float32, Float32)
    | Day(Float32, Float32, Float32)
    | Hour(Float32, Float32, Float32, Float32) 
}

/* 
Define a generic enum type 'TimeUnit3<T1, T2>' who has four constructors: 'Year(T1)', 'Month(T1, T2)', 'Day(T1, T2, T2)' and 'Hour(T1, T2, T2, T2)'.
*/
enum TimeUnit3<T1, T2> { 
    | Year(T1)
    | Month(T1, T2)
    | Day(T1, T2, T2)
    | Hour(T1, T2, T2, T2) 
}

Pay attention to the following points about the enum type:

  1. The enum type can be defined only in top-level.

  2. The constructor definition without parentheses or parameters is not supported. The type of the constructor without parameters is the defined enum type and is not considered as the function type. A constructor with parameters has a function type, but cannot be used as a first-class citizen. For example:

enum E {
    | mkE // OK. The type of mkE is E but not () -> E.
    | mkE() // Syntax error.
}

enum E1 {
    | A
}
let a = A // ok, a: E1

enum E2 {
    | B(Bool)
}
let b = B // error

enum E3 {
    | C
    | C(Bool)
}
let c = C // ok, c: E3
  1. As a user-defined type, the enum type does not support == for equality or != for inequality by default. You can overload the == or != operator (see [Operator Overloading]) to enable the custom enum type to support == or !=.

  2. The same enum supports constructor overloading, but only the number of parameters is involved in overloading. That is, multiple constructors with the same name can be defined in the same enum, but the number of parameters must be different. (A constructor without parameters can be overloaded with other constructors although it is not a function type.) For example:

enum TimeUnit4 { 
    | Year
    | Year(Int32) 		                     // ok
    | Year(Float32) 	                     // error: redeclaration of 'Year'
    | Month(Int32, Float32)                  // ok
    | Month(Int32, Int32) 	                 // error: redeclaration of 'Month'
    | Month(Int32) 			                 // ok
    | Day(Int32, Float32, Float32)           // ok
    | Day(Float32, Float32, Float32) 	     // error: redeclaration of 'Day'
    | Day(Float32, Float32) 			     // ok
    | Hour(Int32, Float32, Float32, Float32) // ok
    | Hour(Int32, Int32, Int32, Int32) 	     // error: redeclaration of 'Hour'
    | Hour(Int32, Int32, Int32) 		     // ok
}
  1. The enum type supports recursion and mutual recursion. For example:
// recursive enum
enum TimeUnit5 { 
    | Year(Int32)
    | Month(Int32, Float32)
    | Day(Int32, Float32, Float32)
    | Hour(Int32, Float32, Float32, Float32)
    | Twounit(TimeUnit5, TimeUnit5) 
}

// mutually recursive enums
enum E1 {
    A | B(E2)
}
enum E2 {
    C(E1) | D(E1)
}
  1. The enum type cannot be inherited.

  2. The enum type can implement interfaces. For details about the interfaces, see [Classes and Interfaces].

Accessing the Enum Value

The first method is to add the enum name to the constructor name. For example:

let time1 = TimeUnit1.Year
let time2 = TimeUnit2.Month(1.0, 2.0)
let time3 = TimeUnit3<Int64, Float64>.Day(1, 2.0, 3.0)

The second method is to omit the enum name. For example:

let time4 = Year                             // syntax sugar of 'TimeUnit1.Year'
let time5 = Month(1.0, 2.0)                  // syntax sugar of 'TimeUnit2.Month(1.0, 2.0)'
let time6 = Day<Int64, Float64>(1, 2.0, 3.0) // syntax sugar of 'TimeUnit3<Int64, Float64>.Day(1, 2.0, 3.0)'

Note: The Cangjie language supports type derivation of generic parameters. Therefore, the generic parameter <T> can be omitted when the preceding method 1 and method 2 are used for access.

The second method must meet the following rules:

If there is no conflict with other variable names, function names, type names, or package names, you can omit the type prefix. Otherwise, the variable name, function name, type name, or package name is preferred.

package p
}

func g() {
    let a = A // ok, find p.A
    let b = E.A // ok
    let c = p.A // ok
    F(1) // ok, find p.F
    E.F(1) // ok
    p.F(1) // ok
    let x: C // ok, find p.C
    let y: p.C // ok
}

When the enum constructor is used without the type prefix, the generic parameters of the enum type can be declared after the constructor name.

enum E<T> {
    | A(T)
    | B(T)
}

func f() {
    let a = A(1)        // ok, a: E<Int64>
    let b = A<Int32>(2) // ok, b: E<Int32>
}

Deconstructing Enums

The match expression and enum pattern can be used to deconstruct the enum type. For details, see [Patterns].

The following is an example of deconstructing the TimeUnit1 and TimeUnit2 defined in the preceding section:

let time12 = TimeUnit1.Year
let howManyHours = match (time12) {
    case Year => 365 * 24 // matched
    case Month => 30 * 24
    case Day => 24
    case Hour => 1
}

let time13 = TimeUnit2.Month(1.0, 1.5)
let howManyHours = match (time13) {
    case Year(y) => y * 365.0 * 24.0
    case Month(y, m) => y * 365.0 * 24.0 + m * 30.0 * 24.0 // matched
    case Day(y, m, d) => y * 365.0 * 24.0 + m * 30.0 * 24.0 + d * 24.0
    case Hour(y, m, d, h) => y * 365.0 * 24.0 + m * 30.0 * 24.0 + d * 24.0 + h
}

Other Members of Enum

You can define member functions, operator functions, and member attributes in the enum type.

  • For details about how to define a common member function, see [Function Definition].
  • For details about the syntax for defining an operator function, see [Operator Overloading].
  • For details about the syntax for defining member attributes, see [Attribute Definition].

The following are some simple examples of enum definitions:

enum Foo {
    A | B | C
    func f1() {
        f2(this)
    }
    static func f2(v: Foo) {
        match (v) {
            case A => 0
            case B => 1
            case C => 2
        }
    }
    prop item: Int64 {
        get() {
            f1()
        }
    }
}

main() {
    let a = Foo.A
    a.f1()
    Foo.f2(a)
    a.item
}

A constructor in the enum type, a static member function, and an instance member function cannot be overloaded. Therefore, the names of constructors in the enum type, static member functions, instance member functions, and member attributes must be unique.

Option

Option type is a generic enum type.

enum Option<T> { Some(T) | None }

Some and None are two constructors. Some(T) indicates a state with a value, and None indicates a state without a value. The type variable T is instantiated into different types, and different Option types, for example, Option<Int32> and Option<String>, are obtained.

Another method of writing the Option type is to add ? before the type name. That is, for any Type, ?Type is equivalent to Option<Type>. For example, ?Int32 is equivalent to Option<Int32>, ?String is equivalent to Option<String>, and so on.

Pay attention to the following points about the Option type:

  1. Although T and Option<T> are of different types, you can directly specify a value of the T type when a value of the Option<T> type is required at a certain position. The compiler encapsulates the value of the T type into Some constructor of the Option<T> type. For example, the following definition is valid:
let v1: ?Int64 = 100
let v2: ??Int64 = 100
  1. For two unequal types T1 and T2, even if there is a subtype relationship between them, there is no subtype relationship between Option<T1> and Option<T2>.

The Option type complies with the usage rules of the generic enum type. For example, you can define a series of variables of different Option types.

let opInt32_1 = Some(100)           // The type of 'opInt32_1' is 'Option<Int32>'
let opInt32_2 = Option<Int32>.None  // The type of 'opInt32_2' is 'Option<Int32>'
let opChar = Some('m')              // The type of 'opChar' is 'Option<Rune>'
let opBool = Option<Bool>.None      // The type of 'opBool' is 'Option<Bool>'
let opEnum = Some(TimeUnit1.Year)   // The type of 'opEnum' is 'Option<TimeUnit1>'
Deconstructing Option Values

Several methods are provided to facilitate the deconstruction of option values: pattern matching, getOrThrow function, coalescing operator (??), and question mark operator (?).

  1. The Option type is an enum type. Therefore, the enum pattern in the pattern matching expression can be used to deconstruct the Option value. For details, see [Enum Pattern].
let number1 = match (opInt32_1) {
    case Some(num) => num // matched
    case None => 0
}

let number2 = match (opInt32_2) {
    case Some(num) => num 
    case None => 0        // matched
}

let enumValue = match (opEnum) {
    case Some(tu) => match (tu) {
                          case Year => "Year"   // matched
                          case Month => "Month"
                          case Day => "Day"
                          case Hour => "Hour"
                      }
    case None => "None"
}
  1. For the expression e of the Option<T> type, the getOrThrow() or getOrThrow(exception: ()->Exception) function is called to deconstruct e. If the value of e is equal to that of Option<T>.Some(v), the value of e.getOrThrow() or e.getOrThrow(lambdaArg) is equal to that of v. If the value of e is equal to that of Option<T>.None, e.getOrThrow() throws a NoneValueException exception during running (e.getOrThrow(lambdaArg) throws an exception specified in lambdaArg during running).
let number1 = opInt32_1.getOrThrow()                 // number1 = 100
let number2 = opInt32_2.getOrThrow()                 // throw NoneValueException
let number3 = opInt32_2.getOrThrow{ MyException("Get None value") } // throw MyException
  1. For the e1 expression of the Option<T> type and the e2 expression of the T type, the type of the e1 ?? e2 expression is T. When the value of e1 is equal to that of Option<T>.Some(v), the value of e1 ?? e2 is equal to that of v. When the value of e1 is equal to that of Option<T>.None, the value of e1 ?? e2 is equal to that of e2. About ?? For details about operators, see [coalescing Expressions].
let number1 = opInt32_1 ?? 0  // number1 = 100
let number2 = opInt32_2 ?? 0  // number2 = 0
  1. The question mark operator (?) is a unary suffix operator.? It must be used together with the suffix operators ., (), {}, or [] so that Option<T> supports these suffix operators. For an expression e of the Option type, e?.b indicates that Option<T>.Some(b) is obtained when e is Some; otherwise, Option<T>.None is obtained (T is of the b type). The same rule applies to other operators. About ? For details about operators, see [Question Mark Operators].
class C {
    var item = 100
}
let c = C()
let c1 = Some(c)
let c2 = Option<C>.None
let r1 = c1?.item   // r1 = Option<Int64>.Some(100)
let r2 = c2?.item   // r2 = Option<Int64>.None

Mutable Types

The following sequentially describes mutable types in the Cangjie programming language.

Array

The Cangjie programming language uses the generic type Array<T> to represent the Array type. The Array type is used to store a series of elements of the same type (or having a common parent class). The description of Array<T> is as follows:

  1. The elements in Array<T> are ordered and can be accessed by index (starting from 0).

  2. The length of the Array type is fixed. That is, once an Array instance is created, the length cannot be changed.

  3. Array is a reference type and is defined as a variable of the reference type. The variable name stores the reference pointing to the data value. Therefore, when a value is assigned or a function specifies parameters, the Array type copies the reference.

  4. When the type variable T is instantiated into different types, different Array types are obtained. For example, Array<Int32> and Array<Float64> indicate Array of the Int32 type and Array of the Float64 type, respectively.

  5. When Type in Array<T> supports == for equality and != for inequality, Array<T> supports == and !=. Otherwise, Array<T> does not support == and !=. If == and != are used, an error is reported during compilation. Two instance values for Array<T> of the same type are equal only when all elements in the same position (index) are equal (indicating that their lengths are equal).

  6. A multidimensional Array is defined as an Array of an Array and is represented by Array<Array<...>>. For example, Array<Array<Int32>> indicates a two-dimensional Array of the Int32 type.

  7. When ElementType has subtypes, Array<ElementType> can store instances of any ElementType subtype. For example, Array<Object> can store instances of any Object subtype.

Creating an Array Instance

You can create an Array instance in either of the following ways.

Constructors
// Array<T>()
let emptyArr1 = Array<Int64>()  // create an empty array whose type is Array<Int64>
let emptyArr2 = Array<String>() // create an empty array whose type is Array<String>

// Array<T>(size: Int64, initElement: (Int64)->T)
let array3 = Array<Int64>(3) { i => i * 2 } // 'array3' has 3 elements: 0, 2, 4
let array4 = Array<String>(2) { i => "$i" } // 'array4' has 2 elements: "0", "1" 
Array Literals

Array literals are in the format of [element1, element2, ..., elementN]. Multiple elements are separated by commas (,). Each element can be a common expression expressionElement. The syntax of an array literal is defined as follows:

arrayLiteral : '[' (elements)? ']' ;
elements    : element (',' element)* ;
element     : expression ;

Each element of the Array literal is an expression.

let emptyArray: Array<Int64> = []    // empty Array<Int64>
let array0 = [1, 2, 3, 3, 2, 1]      // array0 = [1, 2, 3, 3, 2, 1]
let array1 = [1 + 3, 2 + 3, 3 + 3]   // array1 = [4, 5, 6]
  • When the context does not have specific type requirements, if the minimum common parent type of all elements is T, the type of the Array literal is Array<T>.
  • If the context has specific type requirements, all element types must be subtypes of the element type required by the context.

Accessing Elements in an Array

For the arr instance of the Array type, elements in arr can be accessed in the following ways:

Element accessing at a specific position: Access an element at a specific position using arr[index1]...[indexN] (index1, ..., indexN are index expressions of the Int64 type). For example:

let array5 = [0, 1]
let element0 = array5[0]      // element0 = 0
array5[1] = 10                // change the value of the second element of 'array5' through index

let array6 = [[0.1, 0.2], [0.3, 0.4]]
let element01 = array6[0][1]  // element01 = 0.2
array6[1][1] = 4.0            // change the value of the last element of 'array6' through index

Iterative accessing: Iteratively access elements in arr using the for-in expression (see [for-in Expressions]). For example:

func access() {
    let array8 = [1, 8, 0, 1, 0]
    for (num in array8) {
        print("${num}")   // output: 18010
    }
}

Size of the Accessed Array

The number of elements in arr can be returned in arr.size mode.

let array9 = [0, 1, 2, 3, 4, 5]
let array10 = [[0, 1, 2], [3, 4, 5]]
let size1 = array9.size  // size1 = 6
let size2 = array10.size // size2 = 2

Slice of an Array

When a value of the Range<Int64> type is used as the Array index, a segment (called slicing) of the Array is truncated. Note that:

  • The value of step must be 1. Otherwise, an exception will be thrown.
  • The type returned by slicing is the same as that of Array and is the reference of the original Array. Modifications on elements in a slice affect the original array.
  • When start..end is used as the index of Array, if start is omitted, the value of start is set to 0. If end is omitted, the value of end is set to the length of Array.
  • When start..=end is used as the index of Array, if start is omitted, the value of start is set to 0. end in the start..=end format cannot be omitted.
  • If the index is an empty range, an empty Array is returned.
let array7 = [0, 1, 2, 3, 4, 5]

func slicingTest() {
    array7[0..5]      // [0, 1, 2, 3, 4]
    array7[0..5:1]    // [0, 1, 2, 3, 4]
    array7[0..5:2]    // runtime exception
    array7[5..0:-1]   // runtime exception
    array7[5..0:-2]   // runtime exception
    array7[0..=5]     // [0, 1, 2, 3, 4, 5]
    array7[0..=5:1]   // [0, 1, 2, 3, 4, 5]
    array7[0..=5:2]   // runtime exception
    array7[5..=0:-1]  // runtime exception
    array7[5..=0:-2]  // runtime exception
    array7[..4]       // [0, 1, 2, 3]
    array7[2..]       // [2, 3, 4, 5]
    array7[..]        // [0, 1, 2, 3, 4, 5]
    array7[..4:2]     // error: the 'range step' is not allowed here.
    array7[2..:-2]    // error: the 'range step' is not allowed here.
    array7[..:-1]     // error: the 'range step' is not allowed here.

    array7[..=4]      // [0, 1, 2, 3, 4]
    array7[..=4:2]    // error: the 'range step' is not allowed here.

    array7[0..5:-1]   // runtime exception 
    array7[5..=0]     // [] 
    array7[..0]       // [] 
    array7[..=-1]     // []
    let temp: Array<Int64> = array7[..]
    temp[0] = 6 // temp == array7 == [6, 1, 2, 3, 4, 5]
}

When slicing is used to assign values, the following usages are supported:

  • If the expression on the right of = is of the array element type, the value of the expression is used as the element that covers the slice range.
  • If the type of the expression on the right of = is the same as that of the array, the array will be copied to overwrite the current slice range. In this case, the size of the expression on the right must be the same as that of the slice range. Otherwise, an exception will be thrown during running.
let arr = [1, 2, 3, 4, 5]
arr[..] = 0
// arr = [0, 0, 0, 0, 0]
arr[0..2] = 1
// arr = [1, 1, 0, 0, 0]
arr[0..2] = [2, 2]
// arr = [2, 2, 0, 0, 0]
arr[0..2] = [3, 3, 3] // runtime exception
arr[0..2] = [4] // runtime exception

let arr2 = [1, 2, 3, 4, 5]
arr[0..2] = arr2[0..2] // ok
// arr = [1, 2, 0, 0, 0]
arr[0..2] = arr2 // runtime exception
arr[..] = arr2

VArray

The Cangjie programming language uses the generic type VArray<T, $N> to represent the VArray type. The VArray type is used to store a series of elements of the same type (or having a common parent class). The description of VArray<T, $N> is as follows:

  1. The elements in VArray<T, $N> are ordered and can be accessed by index (starting from 0). If the provided index is greater than or equal to its length and the index can be deduced during compilation, a compilation error is reported. Otherwise, an exception is thrown during running.

  2. The length of the VArray<T, $N> type is a part of the type. N indicates the length of VArray. It must be an integer literal and is marked by the fixed syntax $ plus a number. If the provided integer literal is a negative number, an error is reported during compilation.

  3. VArray is a variable of the value type. The variable name stores the data value. Therefore, when a value is assigned or a function specifies parameters, the value is copied for the VArray type.

  4. When the type variable T is instantiated into different types or the length of the $N annotation is different, different VArray types are obtained. For example, VArray<Int32, $5> and VArray<Float64, $5> are different types of VArray. VArray<Int32, $2> and VArray<Int32, $3> are different types of VArray.

  5. When the generic variable T of VArray is of the VArray type, it indicates a multi-dimensional VArray type.

Creating a VArray Instance

VArray can also use the Array literal to create an instance. This method can be used only when the literal is of the VArray type in the context. Otherwise, the literal is of the Array type.

The length of VArray must be the literal type of Int64 and must be the same as the length of the provided literal array. Otherwise, a compilation error is reported.

let arr1: VArray<Int64, $5> = [1,2,3,4,5] 
let arr2: VArray<Int16, $0> = []
let arr3: VArray<Int16, $0> = [1] // error
let arr4: VArray<Int16, $-1> = [] // error

In addition, VArray can be used to create instances in the form of constructors.

// VArray<T, $N>(initElement: (Int64)->T)
let arr5: VArray<Int64, $5> = VArray<Int64, $5> { i => i } // [0, 1, 2, 3, 4]
// VArray<T, $N>(item!: T)
let arr6: VArray<Int64, $5> = VArray<Int64, $5>(item: 0) // [0, 0, 0, 0, 0]

VArray does not allow the default type parameter <T, $N>.

Accessing Elements in a VArray

For the arr instance of the VArray type, elements in arr can be accessed in the following ways:

Access an element at a specific position using arr[index1]...[indexN] (index1,...,indexN are index expressions of the Int64 type). For example:

Note that the index operation of VArray returns the copy of the specified element. It indicates that if the element is of the value type, the index value will get a new instance that cannot be modified. For multi-dimensional VArray types, the inner VArray cannot be modified by arr[n][m] = e because the inner VArray obtained by arr[n] is a new VArray instance that has been copied.

var arr7: VArray<Int64, $2> = [0, 1]
let element0 = arr7[0]      // element0 = 0
arr7[1] = 10                // change the value of the second element of 'arr7' through index

// Get and Set of multi-dimensional VArrays.
var arr8: VArray<VArray<Int64, $2>, $2> = [[1, 2], [3, 4]]
let arr9: VArray<Int64, $2> = [0, 1]
let element1 = arr8[1][0]   // element1 = 3
arr8[1][1] = 5              // error: function call returns immutable value
arr8[1] = arr9              // arr8 = [[1, 2], [0, 1]]

Obtaining the VArray Lengths

The number of elements in arr can be returned in arr.size mode.

let arr9: VArray<Int64, $6> = [0, 1, 2, 3, 4, 5]
let size = arr9.size  // size = 6

VArray in the Function Signature

When VArray is used as a parameter or return value of a function, the length of VArray must be specified.

func mergeVArray(a: VArray<Int64,$2>, b: VArray<Int64, $3>): VArray<Int64, $5> {
    var ret = VArray<Int64, $5>(item: 0)
    for(i in 0..2) {
        ret[i] = a[i]
    }
    for (i in 0..3) {
        ret[a.size + i] = b[i]
    }
    return ret
}

Struct

The struct type is a mutable type. A series of member variables and member functions can be defined in the struct type. The syntax for defining the struct type is as follows:

structDefinition
    : structModifier? 'struct' identifier typeParameters? ('<:' superInterfaces)? genericConstraints? structBody
    ;

structBody
    : '{'
        structMemberDeclaration*
        structPrimaryInit? 
        structMemberDeclaration*
      '}'
    ; 

structMemberDeclaration
    : structInit
    | staticInit
    | variableDeclaration
    | functionDefinition
    | operatorFunctionDefinition
    | macroExpression
    | propertyDefinition
    ;    

structModifier is the modifier of the struct type, struct is the keyword, identifier is the name of the struct type, and typeParameters and genericConstraints are the type variable list and type variable constraints, respectively. For details, see [Generics].In structBody, you can define member variables (variableDeclaration), member attributes (propertyDefinition), main constructors (structPrimaryInit), constructors (structInit), and member functions (including common member functions and operator functions).

Pay attention to the following points about the struct type:

  1. struct is a variable of the value type. The variable name stores the data value. Therefore, when a value is assigned or a function specifies parameters, the value is copied for the struct type.

  2. The struct type can be defined only in top-level.

  3. As a user-defined type, the struct type does not support == for equality or != for inequality by default. You can overload the == or != operator (see [Operator Overloading]) to enable the custom struct type to support == or !=.

  4. The struct type cannot be inherited.

  5. The struct type can implement the [Interface].

  6. If the type of one or more non-static member variables of a struct type references the struct itself, the struct type is called a recursive struct type. For multiple struct types, if the types of their non-static member variables form a circular reference, the struct types form a recursion. The struct type defined by recursion (or mutual recursion) is invalid unless at least one T_i is encapsulated in the class, interface, enum, or function type on each T_1, T_2, ..., T_N of the recursion chain. That is, the class, interface, enum, or function type can be used to implement recursion (or mutual recursion). The struct definition is legalized.

The following is an example of the struct type:

struct Rectangle1 { 
    let width1: Int32
    let length1: Int32
    let perimeter1: () -> Int32

    init (width1: Int32, length1: Int32) {
        this.width1 = width1
        this.length1 = length1
        this.perimeter1 = { => 2 * (width1 + length1) }
    }
    init (side: Int32) {
        this(side, side)
    }
    func area1(): Int32 { width1 * length1 }
}

// Define a generic struct type.
struct Rectangle2<T> {
    let width2: T
    let length2: T

    init (side: T) {
      this.width2 = side
      this.length2 = side
    }

    init (width2!: T, length2!: T) {
        this.width2 = width2
        this.length2 = length2
    }
}

The following is an example of the struct type definition for recursion and mutual recursion:

struct R1 { // error: 'R1' cannot have a member that recursively contains it
    let other: R1
}
struct R2 { // ok
    let other: Array<R2>
}

struct R3 { // error:  'R3' cannot have a member that recursively contains it
    let other: R4
}
struct R4 { // error:  'R4' cannot have a member that recursively contains it
    let other: R3
}

struct R5 { // ok
    let other: E1
}
enum E1 { // ok
    A(R5)
}

struct Member Variables

The syntax for defining a member variable is as follows:

variableDeclaration
    : variableModifier* NL* (LET | VAR) NL* patternsMaybeIrrefutable ((NL* COLON NL* type)? (NL* ASSIGN NL* expression) | (NL* COLON NL* type))
    ;

When defining member variables, pay attention to the following points:

  1. A member variable defined outside the primary constructor can have an initial value or not. If there is an initial value, the initial value expression can reference only the initialized member variables defined before it and the static member functions in struct.

Constructors

In the Cangjie programming language, there are two types of constructors: primary constructor and init constructor (constructor for short).

Primary Constructor

The syntax of the primary constructor is defined as follows:

structPrimaryInit
    : (structNonStaticMemberModifier)? structName '('
      structPrimaryInitParamLists? ')'
 	  '{'
 	       expressionOrDeclarations?
 	  '}'
	;

structName
    : identifier
    ;

structPrimaryInitParamLists
    : unnamedParameterList  (','  namedParameterList)? 
       (',' structNamedInitParamList)?
    | unnamedParameterList (',' structUnnamedInitParamList)? 
       (',' structNamedInitParamList)?
    | structUnnamedInitParamList (',' structNamedInitParamList)?
    | namedParameterList (',' structNamedInitParamList)?
    | structNamedInitParamList
    ;

structUnnamedInitParamList
    : structUnnamedInitParam (',' structUnnamedInitParam)*
    ;

structNamedInitParamList
    : structNamedInitParam (',' structNamedInitParam)*
    ;

structUnnamedInitParam
    : structNonStaticMemberModifier?  ('let' | 'var')  identifier ':' type
    ;

structNamedInitParam
    : structNonStaticMemberModifier?   ('let' | 'var')  identifier '!' ':' type
      ('=' expression)?
    ;

The definition of the primary constructor consists of the following parts:

  1. Modifier (Optional): It can be modified by all access modifiers. The default accessibility is internal. For details, see Access Modifiers in [Packages and Modules].

  2. Primary constructor name: same as the type name. Do not use the func keyword before the primary constructor name.

  3. Parameter list: Different from the init constructor, the primary constructor has two types of parameters: ordinary parameters and member variable parameters. The syntax and semantics of ordinary parameters are the same as those of parameters in the function definition.

Member variable parameters are introduced to reduce code redundancy. The definition of a member variable parameter includes the definition of a parameter and a member variable. In addition, it indicates the semantics of assigning a value to a member variable through a parameter. Omitted definitions and expressions are automatically generated by the compiler.

  • The syntax of member variable parameters is the same as that of member variable definition. In addition, ! can be used to indicate whether a member variable parameter is a named parameter.

  • The modifiers of the member variable parameter are public, private, protected, and internal. For details, see Access Modifiers in [Packages and Modules].

  • Member variable parameters only support non-static member variables. That is, they cannot be modified by static.

  • A member variable parameter cannot have the same name as a member variable outside the primary constructor.

  • A member variable parameter can have no initial value. This is because the compiler generates a corresponding constructor for the primary constructor and assigns values to member variables in the constructor body.

  • A parameter of a member variable can have an initial value. The initial value expression can reference other parameters or member variables (excluding instance member variables defined outside the primary constructor) that have been defined before the member variable is defined, but cannot modify the values of these parameters and member variables. Note that the initial value of the member variable parameter is valid only in the primary constructor and is not included in the member variable definition.

  • Member variable parameters cannot be followed by ordinary parameters. In addition, the parameter sequence must comply with that defined in the function. Parameters of named variables cannot be followed by non-named parameters.

  1. Main constructor body: The main constructor cannot call other constructors in this struct. Declarations and expressions can be written in the main constructor body. The declarations and expressions must meet the requirements of the init constructor.

The following is an example of the primary constructor definition:

struct Test{
    static let counter: Int64 = 3
    let name: String = "afdoaidfad"
    private Test( 
        name: String,             // regular parameter  
        annotation!: String = "nnn",   // regular parameter  
        var width!: Int64 = 1,     // member variable parameter with initial value
        private var length!: Int64,    // member variable parameter
        private var height!: Int64 = 3   // member variable parameter 
    ) {}
}

The primary constructor is the syntactic sugar of the init constructor. The compiler automatically generates the definitions of the constructor and member variables corresponding to the primary constructor. The constructor is automatically generated as follows:

  • Its modifier is the same as that of the primary constructor.
  • The sequence of parameters from left to right is the same as that declared in the parameter list of the primary constructor.
  • The form of the constructor body is as follows:
    • Values are assigned to member variables. The syntax format is this.x = x, where x is the member variable name.
    • Then, other code of the main constructor body is used.
struct B<X,Y> { 
    B(
        x: Int64,          // primary constructor, it's name is the same as the struct
        y: X,
        v!: Int64 = 1,     // regular parameter
        private var z!: Y  // member variable parameter
    ) {}

    /* The corresponding init constructor with primary constructor auto-generated
    by compiler.
    
    private var z: Y    // auto generated member variable definition
    init( x: Int64, y: X, v!: Int64 = 1, z!: Y) { // auto generated named parameter definition
        this.z = z // auto generated assign expression of member variable
    }
    */
}

A struct can define only one primary constructor. In addition to the primary constructor, other constructors can be defined as usual, but other constructors and the constructor corresponding to the primary constructor must be overloaded.

init Constructor

In addition to the main constructor, you can also customize a constructor. The constructor is defined by using the keyword init, parameter list, and function body. Multiple constructors can be defined in a struct, but they and the constructor corresponding to the main constructor must form an overload. For details about function overload, see [Function Overload Definition]. Note:

  1. The parameters of a constructor can have default values. Do not use the instance member variable this.variableName and its syntactic sugar variableName as the default values of constructor parameters.

  2. Before all instance member variables are initialized, the constructor cannot use implicit parameter passing or functions or lambda that capture this. However, this.variableName or its syntactic sugar variableName can be used to access the initialized member variable variableName.

  3. The lambda and nested functions in constructors cannot capture this, and this cannot be used independently as an expression.

  4. In a constructor, this can be used to call other constructors. If this function is called, it must be called at the first expression in the constructor body. No expression or declaration is allowed before this function is called. Do not call expressions to call constructors through this outside the constructor.

  5. If the constructor does not explicitly call other constructors, ensure that all instance member variables declared by the struct are initialized before return. Otherwise, an error is reported during compilation.

  6. The compiler performs dependency analysis on the calling relationships between all constructors. If a constructor is called cyclically, an error is reported during compilation.

  7. The return type of the constructor is Unit.

If a struct does not define a primary constructor or an init constructor, it attempts to generate a parameterless constructor (public-modified). If the instance member variable of the struct does not have an initial value, an error is reported during compilation.

Other Members of Struct

You can also define member functions, operator functions, member attributes, and static initializers in struct.

  • For details about how to define a common member function, see [Function Definition].
  • For details about the syntax for defining an operator function, see [Operator Overloading].
  • For details about the syntax for defining member attributes, see [Attribute Definition].
  • For details about the syntax for defining a static initializer, see [Static Initializers].

Modifiers in the Struct

A struct and its members in a class can be modified using access modifiers. For details, see Access Modifiers in [Packages and Modules].

Member functions and variables can be modified using static. These members are static members of the struct type, not members of the struct instance. Outside the struct definition, instance member variables and instance member functions can be accessed only through the struct instance, and static member variables and static member functions can be accessed only through the name of the struct type.

In addition, a function can be modified by mut, which is a special instance member function. For details about the mut function, see [mut Functions].

The struct constructor and the member variables defined in the main constructor can be modified only by using access modifiers.

Instantiation of Structs

After defining the struct type, you can create a struct instance. The struct instance can be defined in either of the following methods based on whether the instance contains type variables:

  1. Define an instance of the non-general struct: StructName(arguments). StructName indicates the name of the struct type, and arguments indicates the argument list. StructName(arguments) calls the corresponding constructor based on the calling rule of overloaded functions (see [Function Overloading Resolution]), and then generates an instance of StructName. For example:
let newRectangle1_1 = Rectangle1(100, 200) // Invoke the first custom constructor.
let newRectangle1_2 = Rectangle1(300)      // Invoke the second custom constructor.
  1. Define an instance of the generic struct: StructName<Type1, Type2, ..., TypeK>(labelValue1, labelValue2, ..., labelValueN). The only difference from defining an instance of a non-general struct is that the generic parameter needs to be instantiated. For example:
let newRectangle2_1 = Rectangle2<Int32>(100) // Invoke the custom constructor.
let newRectangle2_1 = Rectangle2<Int32>(width2: 10, length2: 20) // Invoke another custom constructor.

Note that if the variable structInstance of the struct type is defined using let, the value of the member variable varName cannot be changed using structInstance.varName = expr (even if varName is defined using var). If both structInstance and varName are defined using var, the value of varName can be changed using structInstance.varName = expr.

Class and Interface Types

class and interface are reference types and are defined as variables of the reference type. The variable name stores the reference pointing to the data value. Therefore, when a value is assigned or a function specifies parameters, class and interface copy the reference.

For details, see [Classes and Interfaces].

Type Conversion

As a strongly-typed language, the Cangjie programming language supports only explicit type conversion (also referred to as forcible type conversion). For the value type, valueType(expr) can be used to convert the type of the expression expr to valueType. For class and interface, [as Operators]) can be used to implement static type conversion. For the value type, valueTypeA can be converted to valueTypeB. That is, the conversion rule for converting valueTypeA to valueTypeB is defined. For the two value types that do not have the conversion rule, they cannot be converted. For class and interface, if a parent-child type relationship exists between two types in the inheritance relationship diagram, the two types can be converted (the conversion may fail). Otherwise, the two types cannot be converted.

The following describes the type conversion rules between value types and between class and interface types. For other types that are not mentioned, Cangjie does not support type conversion between them in the preceding two methods.

Conversion Between Value Types

For numeric types, only the following conversions are supported:

  1. Conversion from the Rune type to the UInt32 type.

  2. Conversion from integer types (including Int8, Int16, Int32, Int64, IntNative, UInt8, UInt16, UInt32, UInt64, and UIntNative) to the Rune type.

  3. Bidirectional conversion between all numeric types (including Int8, Int16, Int32, Int64, IntNative, UInt8, UInt16, UInt32, UInt64, UIntNative, Float16, Float32, and Float64).

The conversion from Rune to UInt32 uses the UInt32(e) mode. e is an expression of the Rune type. The result of UInt32(e) is an integer of the UInt32 type corresponding to the Unicode scalar value of e.

The conversion from the integer type to the Rune type uses the Rune(num) mode. The num type can be any integer type. Only when the value of num is in the range [0x0000,0xD7FF] or [0xE000,0x10FFFF] (that is, Unicode scalar value), the character represented by the corresponding Unicode scalar value is returned. Otherwise, an error is reported during compilation (the value of num can be determined during compilation) or an exception is thrown during running.

main() {
    var c: Rune = 'a'
    var num: UInt32 = 0
    num = UInt32(c) // num = 97
    num -= 32       // num = 65
    c = Rune(num)   // c = `A`
    return 0
}

To ensure type security, the Cangjie programming language does not support implicit type conversion between numeric types (the type of a numeric literal is inferred from the context, which is not implicit type conversion). To convert a numeric type to another numeric type, you must use the explicit method NumericType(expr) to forcibly convert the expr type to the NumericType type (NumericType indicates any numeric type). If the conversion is successful, a new value of the NumericType type constructed from the expr type is returned. The syntax for numeric type conversion is defined as follows:

numericTypeConvExpr
    : numericTypes '(' expression ')'
    ;
numericTypes
    : 'Int8' | 'Int16' | 'Int32' | 'Int64' | 'IntNative' | 'UInt8' | 'UInt16' | 'UInt32' | 'UInt64' | 'UIntNative' | 'Float16' | 'Float32' | 'Float64'
    ;

If the size relationship between numeric types is defined based on the number of bits occupied by numeric types (a larger number of bits indicates a larger type, and a smaller number of bits indicates a smaller type), the Cangjie programming language supports the following type conversion:

  1. Bidirectional conversion between signed integer types: When a small value is converted to a large value, the value result remains unchanged. If the value result exceeds the representation range of the small value type, the overflow processing policy is determined based on the attribute macro in the context. By default, the exception throwing policy is used. For details about different overflow policies, see [Arithmetic Expressions]. The following uses the conversion between Int8 and Int16 as an example. When an overflow occurs, the policy of throwing an exception is used.
main() {
    var i8Number: Int8 = 127
    var i16Number: Int16 = 0
    i16Number = Int16(i8Number) 	// ok: i16Number = 127
    i8Number = Int8(i16Number) 		// ok: i8Number = 127
    i16Number = 128
    i8Number = Int8(i16Number) 		// throw an ArithmeticException
    return 0
}
  1. The rule of the bidirectional conversion between unsigned integer types is the same as the preceding rule. The following uses the conversion between UInt16 and UInt32 as an example (the same rule applies to other cases):
main() {
    var u16Number: UInt16 = 65535
    var u32Number: UInt32 = 0
    u32Number = UInt32(u16Number) 	// ok: u32Number = 65535
    u16Number = UInt16(u32Number) 	// ok: u16Number = 65535
    u32Number = 65536
    u16Number = UInt16(u32Number)   // throw an ArithmeticException
    return 0
}
  1. Bidirectional conversion between floating-point types uses the round-to-nearest mode. The following is an example of the conversion between Float32 and Float64:
main() {
    var f32Number: Float32 = 1.1
    var f64Number: Float64 = 0.0
    f64Number = Float64(f32Number)   	// f64Number = 1.100000023841858
    f32Number = Float32(f64Number)    	// f32Number = 1.1
    f64Number = 1.123456789
    f32Number = Float32(f64Number) 	    // f32Number = 1.1234568
    f32Number = 4.4E38 					// f32Number = POSITIVE_INFINITY
    f64Number = Float64(f32Number) 	    // f64Number = POSITIVE_INFINITY
    f64Number = 4.4E38
    f32Number = Float32(f64Number) 	    // f32Number = POSITIVE_INFINITY
    f64Number = Float64(f32Number*0.0)
    f32Number = Float32(f64Number) 	    // f32Number = NaN
    return 0
}
  1. Bidirectional conversion between signed integer types and unsigned integer types: Because the representation range of any signed integer type cannot contain the representation range of an unsigned integer type of the same length (or vice versa), the conversion is successful if the value of the expression to be converted is within the range of the target integer type during type conversion. Otherwise, the overflow processing policy is determined based on the attribute macro in the context. By default, the exception throwing policy is used. For details about different overflow processing policies, see [Arithmetic Expression]. The following uses the conversion between Int8 and UInt8 as an example. When an overflow occurs, the policy of throwing an exception is used.
main() {
    var i8Number: Int8 = 127
    var u8Number: UInt8 = 0
    u8Number = UInt8(i8Number) 	// ok: u8Number = 127
    u8Number = 100
    i8Number = Int8(u8Number) 	// ok: i8Number= 100
    i8Number= -100
    u8Number = UInt8(i8Number) 	// throw an ArithmeticException
    u8Number = 255
    i8Number = Int8(u8Number) 	// throw an ArithmeticException
    return 0
}
  1. Conversion from an integer to a floating-point number: The result is a floating-point number as close as possible to the original integer. POSITIVE_INFINITY or NEGATIVE_INFINITY is returned if the value is out of the representation range of the target type.

  2. Conversion from a floating-point number to an integer: The round-toward-zero mode is used to convert a floating-point number to a signed integer. That is, the integer part is retained and the decimal part is discarded. When the integer part exceeds the representation range of the target integer type, the integer overflow policy in the context is used. If the throwing policy is used, an exception is thrown. Otherwise, the conversion rule is as follows:

  • NaN returns 0.
  • If the value is less than the lower limit of the integer value range (including negative infinity), the lower limit of the integer value range is returned.
  • If the value is greater than the upper limit of the integer value range (including positive infinity), the upper limit of the integer value range is returned.
main() {
    var i32Number: Int32 = 1024
    var f16Number: Float16 = 0.0
    var f32Number: Float32 = 0.0
    f16Number = Float16(i32Number) 	// ok: f16Number = 1024.0
    f32Number = Float32(i32Number) 	// ok: f32Number = 1024.0
    i32Number = 2147483647
    f16Number = Float16(i32Number) 	// f16Number = POSITIVE_INFINITY
    f32Number = Float32(i32Number) 	// precision lost: f32Number = 2.14748365E9
    
    f32Number = 1024.1024
    i32Number = Int32(f32Number) 	// ok: i32Number = 1024
    f32Number = 1024e10
    i32Number = Int32(f32Number) 	// throw an Exception
    f32Number = 3.4e40 				// f32Number = POSITIVE_INFINITY
    i32Number = Int32(f32Number) 	// throw an Exception
    f32Number = 3.4e40 * 0.0 		// f32Number = NaN
    i32Number = Int32(f32Number) 	// throw an Exception
    return 0
}

Conversion Between Class and Interface Types

For an instance obj of the class or interface type, if you need to convert its (static) type to another TargetType of the class or interface type, use obj as TargetType.

For details about how to use the as operator and the type conversion rules between the class and interface types, see [as Operators].

Type Aliases

If the name of a type is complex or not intuitive in a specific scenario, you can use a type alias to obtain a simple and intuitive alias for the type. The syntax for defining a type alias is as follows:

typeAlias
	: typeModifier? `type` identifier typeParameters? `=` type
	;

typeModifier is an optional accessibility modifier (namely, public), type is a keyword, identifier is any valid identifier, type is any type visible to top-level, and identifier and type are connected by using =. In addition, you can define a generic alias by adding a type parameter (typeParameters) after identifier. For details, see [Generics].Through the preceding statement, an alias named identifier is defined for type, and identifier and type are considered as the same type. For example:

type Point2D = (Float64, Float64)
type Point3D = (Float64, Float64, Float64)

let point1: Point2D = (0.5, 0.8)
let point2: Point3D = (0.5, 0.8, 1.1)

The preceding type definition does not define a new type. It is only used to define another name for an existing type. The alias and the original type are considered as the same type, and the alias does not affect the use of the original type.

Rules for Defining Type Aliases

  1. The definition of a type alias can be used only in top-level.
func test() {
    type Point2D = (Float64, Float64) 			// error: type alias can only be defined at top-level
    type Point3D = (Float64, Float64, Float64) 	// error: type alias can only be defined at top-level
}
  1. When type is used to define a type alias, the original type must be visible in the position defined by type.
class LongNameClassA {

}
type ClassB = LongNameClassB	// error: use of undeclared type 'LongNameClassB'
  1. When a generic alias is defined, if the generic alias introduces a generic parameter that is not used in the original type, the compiler generates a warning.
type Class1<V> = GenericClassA<Int64, V> 		// ok. ClassA is a generic class
type Class2<Value, V> = GenericClassB<Int64, V> // warning: the type parameter 'Value' in 'Class2<Value, V>' is not used in 'GenericClassB<Int64, V>'
type Int<T> = Int32 				            // warning: the type parameter 'T' in 'Int<T>' is not used in `Int32`
  1. When defining a generic alias, you are not allowed to add generic constraints to the alias and the type parameters in the original type. When using a generic alias, you can add generic constraints as required. In addition, existing generic constraints in the original type are passed to the alias.
type Class1<V> where V <: MyTrait = GenericClassA<Int64, V> // error: generic constraints are not allowed here
type Class2<V> = GenericClassB<Int64, V> where V <: MyTrait // error: generic constraints are not allowed here

type Class3<V> = GenericClassC<Int64, V>
func foo<V> (p: Class3<V>) where V <: MyTrait {             // add generic constraints when 'Class3<V>' is used
    functionBody
}

class ClassWithLongName<T> where T<:MyTrait {
    classBody
}
type Class<T> = ClassWithLongName<T>                       // Class<T> also has the constraint 'where T<:MyTrait'
  1. Circular references (direct or indirect) are not allowed in one or more type definitions. A manner of determining the circular reference is to determine whether the circular reference exists by using a name instead of using a manner of expanding a type.
type Value = GenericClassAp<Int64, Value> 	// error: 'Value' references itself
type Type1 = (Int64)->Type1 		        // error: 'Type1' references itself
type Type2 = (Int64, Type2) 		            // error: 'Type2' references itself
type Type3 = Type4 				            // error: 'Type3' indirectly references itself
type Type4 = Type3
  1. A type alias is considered as a type equivalent to the original type. For example, in the following example, a parameter of the Int type and a parameter of the Int32 type may be directly added. Int is defined as an alias of Int32. Note that aliases cannot be used to overload functions.
type Int = Int32
let numOne: Int32 = 10
let numTwo: Int = 20
let numThree = numOne + numTwo

func add(left: Int, right: Int32): Int { left + right }

func add(left: Int32, right: Int32): Int32 { left + right } 	// error: invalid redeclaration of 'add : (Int32, Int32)->Int32'
  1. The default visibility defined by type is default. To use the type alias defined in this package in other packages, the following conditions must be met: (1) The visibility modifier of the original type in this package is public; (2) The type definition uses the public modifier. In addition, note that the alias can have a different visible range from the original type, but the visible range of the alias cannot be greater than the visible range of the original type.
// a.cj
package A
public class ClassWithLongNameA {
    
}
class ClassWithLongNameB {
    
}
 
public type classA = ClassWithLongNameA 		// ok
type classAInter = ClassWithLongNameA 	// ok 

/* error: classB can not be declared with modifier 'public', as 'ClassWithLongNameB' is internal */
public type classB = ClassWithLongNameB 		

// b.cj
package B
import A.*
let myClassA: A.classA = ClassWithLongNameA()

Usage of Type Aliases

A type alias can be used in any position that the original type can use on the right-hand side of the equal sign (=).

  1. When it is used as a type, for example:

    type A = B
    class B {}
    var a: A = B() // Use typealias A as type B
    
  2. When the type alias refers to a class or struct, it can be used as the constructor name.

    type A = B
    class B {}
    func foo() { A() }  // Use type alias A as constructor of B
    
  3. When the type alias refers to a class, interface, or struct, it can be used as the name to access static members or functions.

    type A = B
    class B {
    	static var b : Int32 = 0;
    	static func foo() {}
    }
    func foo() { 
    	A.foo() // Use A to access static method in class B
    	A.b
    }
    
  4. When the type alias refers to an enum, it can be used as the type name for declaring the constructor of the enum.

    enum TimeUnit {
    	Day | Month | Year
    }
    type Time = TimeUnit
    var a = Time.Day  
    var b = Time.Month   // Use type alias Time to access constructors in TimeUnit 
    

Relationship Between Types

There are two types of relationships: equality and subtype.

Type Equality

For any two types T1 and T2, T1 and T2 are considered equal (denoted as $T1 \equiv T2$) if they meet any of the following conditions:

  • The type alias definition type T1 = T2 exists.

  • In the class definition and extend of class, T1 is the name of class, and T2 is This.

  • The names of T1 and T2 are the same (reflective).

  • $T2 \equiv T1$ (symmetry).

  • The type $Tk$ exists, which meets $T1 \equiv Tk$ and $Tk \equiv T2$ (transitive).

Subtypes

For any two types T1 and T2, T1 and T2 are considered as subtypes (denoted as $T1 <: T2$) if they meet any of the following conditions:

  • $T1 \equiv T2$.

  • T1 is of the Nothing type.

  • Both T1 and T2 are of the Tuple type, and the type at each position of T1 is the subtype of the type at the position corresponding to T2.

  • Both T1 and T2 are of the Function type. The parameter type of T2 is the subtype of the T1 parameter type, and the return type of T1 is the subtype of the T2 return type.

  • T1 is of any class type, and T2 is of the Object type.

  • Both T1 and T2 are of the interface type, and T1 inherits T2.

  • Both T1 and T2 are of the class type, and T1 inherits T2.

  • T2 is of the interface type, and T1 implements T2.

  • The type $Tk$ exists, which meets $T1 <: Tk$ and $Tk <: T2$ (transitive).

Minimum Common Supertype

In a type system with subtypes, the minimum common parent type of two types may be required. For example, the type of the if expression is the minimum common supertype of the types of the two branches. The match expression is similar.

The minimum common supertype between two types is a subtype of all other common supertypes.

The minimum common supertype is defined as follows: For any two types T1 and T2, if the type LUB meets the following rules, LUB is the minimum common supertype of T1 and T2:

  • For any type of T that meets both T1 <: T and T2 <: T, LUB <: T is true.

Note that if a type in the common supertype is not larger than other types, it is only an extremely small one and not necessarily minimum one.

Maximum Common Subtype

Inversion exists in the subtype relationship (see [Type Variation] in [Generics] for its definition).For example, if the parameter type of the function type is inversion, the maximum common subtype of the two types is required.

The maximum common subtype between two types is a supertype of all other common subtypes.

The maximum common subtype is defined as follows: For any two types T1 and T2, if the type GLB meets the following rules, GLB is the maximum common subtype of T1 and T2:

  • For any type of T that meets both T <: T1 and T <: T2, T <: GLB is true.

Note that if a type in the common subtype is not smaller than other types, it is only an extremely large one and not necessarily maximum one.

Type Security

In the case of no data contention, the compiler ensures memory security and type security.

The following is an example in which type security and memory security cannot be ensured:

class C {
    var x = 1
    var y = 2
    var z = 3
}

enum E {
    A(Int64) | B(C)
}

var e = A(1)

main() {
    spawn {
        while (true) {
            e = B(C())      // writing to `e`
            e = A(0)
        }
    }
    while (true) {
        match (e) {         // reading from `e`
            case A(n) =>
                println(n+1)
            case B(c) =>
                c.x = 2
                c.x = 3
                c.x = 4
        }
    }
}

It cannot be ensured because there is data competition between the thread that assigns a value to the variable e and the thread that reads the variable. For more information about data contention, see [Concurrency].