Pattern Overview

For match expressions that contain values to be matched, the patterns supported after case determine the expressiveness of the match expression. In this section, we will introduce the patterns supported by Cangjie, including: constant patterns, wildcard patterns, binding patterns, tuple patterns, type patterns, and enum patterns.

Constant Pattern

Constant patterns can be integer literals, floating-point literals, character literals, Boolean literals, string literals (string interpolation is not supported), and unit literals.

When a constant pattern is used in the match expression that contains a value to be matched, the type of the value represented by the constant pattern must be the same as that of the value to be matched. The matching is successful only when the value to be matched is equal to the value represented by the constant pattern.

In the following example, the exam score level is output based on the value of score (assuming score is an integer multiple of 10 ranging from 0 to 100):

main() {
    let score = 90
    let level = match (score) {
        case 0 | 10 | 20 | 30 | 40 | 50 => "D"
        case 60 => "C"
        case 70 | 80 => "B"
        case 90 | 100 => "A" // Matched.
        case _ => "Not a valid score"
    }
    println(level)
}

The result is as follows:

A
  • When the target of pattern matching is a value whose static type is Rune, both the Rune literal and the single-character string literal can be used as constant patterns representing Rune literals.

    func translate(n: Rune) {
        match (n) {
            case "A" => 1
            case "B" => 2
            case "C" => 3
            case _ => -1
        }
    }
    
    main() {
        println (translate (r"C"))
    }
    

    The result is as follows:

    3
    
  • When the target of pattern matching is a value whose static type is Byte, a string literal representing an ASCII character can be used as a constant pattern representing the Byte literal.

    func translate(n: Byte) {
        match (n) {
            case "1" => 1
            case "2" => 2
            case "3" => 3
            case _ => -1
        }
    }
    
    main() {
        println(translate(51)) // UInt32(r'3') == 51
    }
    

    The result is as follows:

    3
    

Wildcard Pattern

A wildcard pattern is represented by _ and can match any value. The wildcard pattern is typically used as a pattern in the last case to match situations not covered by other cases. In the example of matching the value of score in Constant Pattern, _ in the last case is used to match invalid score values.

Binding Pattern

A binding pattern is represented by id, which is a valid identifier. Similar to wildcard patterns, binding patterns can also match any value, but the matched value is bound to id, allowing access to its bound value after the =>.

In the following example, the binding pattern is used in the last case to bind a non-zero value:

main() {
    let x = -10
    let y = match (x) {
        case 0 => "zero"
        case n => "x is not zero and x = ${n}" // Matched.
    }
    println(y)
}

The result is as follows:

x is not zero and x = -10

When | is used to connect multiple patterns, the binding pattern cannot be used and cannot be nested in other patterns. Otherwise, an error is reported.

main() {
    let opt = Some(0)
    match (opt) {
        case x | x => {} // Error, variable cannot be introduced in patterns connected by '|'
        case Some(x) | Some(x) => {} // Error, variable cannot be introduced in patterns connected by '|'
        case x: Int64 | x: String => {} // Error, variable cannot be introduced in patterns connected by '|'
    }
}

The binding pattern id is equivalent to defining an immutable variable named id (whose scope starts from the introduction point to the end of the case). Therefore, the id cannot be modified after =>. In the following example, the modification of n in the last case is not allowed:

main() {
    let x = -10
    let y = match (x) {
        case 0 => "zero"
        case n => n = n + 0 // Error, 'n' cannot be modified.
                  "x is not zero"
    }
    println(y)
}

For each case branch, the scope level of variables after => is the same as that of variables introduced before =>. Introducing a variable with the same name again after => will trigger a redefinition error. Example:

main() {
    let x = -10
    let y = match (x) {
        case 0 => "zero"
        case n => let n = 0 // Error, redefinition
                  println(n)
                  "x is not zero"
    }
    println(y)
}

Note:

When the identifier of a pattern is an enum constructor, the pattern is matched as an enum pattern instead of a binding pattern. For details about the enum pattern, see Enum Pattern.

enum RGBColor {
    | Red | Green | Blue
}

main() {
    let x = Red
    let y = match (x) {
        case Red => "red" // The 'Red' is enum mode here.
        case _ => "not red"
    }
    println(y)
}

The result is as follows:

red

Tuple Pattern

Tuple patterns are used to match tuple values, defined similarly to tuple literals: (p_1, p_2, ..., p_n), where p_1 to p_n (with n greater than or equal to 2) are patterns (which can be any pattern discussed in this chapter, separated by commas) rather than expressions.

For example, (1, 2, 3) is a tuple pattern that contains three constant patterns, while (x, y, _) is a tuple pattern that contains two binding patterns and one wildcard pattern.

If a tuple value tv and a tuple pattern tp are given, the tp can match the tv only when each value at the corresponding position in tv matches the pattern at that position in tp. For example, (1, 2, 3) can match only the tuple value (1, 2, 3), and (x, y, _) can match any three-element tuple value.

The following example shows how to use the tuple pattern:

main() {
    let tv = ("Alice", 24)
    let s = match (tv) {
        case ("Bob", age) => "Bob is ${age} years old"
        case ("Alice", age) => "Alice is ${age} years old" // Matched, "Alice" is a constant pattern, and 'age' is a variable pattern.
        case (name, 100) => "${name} is 100 years old"
        case (_, _) => "someone"
    }
    println(s)
}

The result is as follows:

Alice is 24 years old

Multiple binding patterns with the same name cannot be introduced to the same tuple pattern. In the following example, case (x, x) in the last case is invalid.

main() {
    let tv = ("Alice", 24)
    let s = match (tv) {
        case ("Bob", age) => "Bob is ${age} years old"
        case ("Alice", age) => "Alice is ${age} years old"
        case (name, 100) => "${name} is 100 years old"
        case (x, x) => "someone" // Error, Cannot introduce a variable pattern with the same name, which will be a redefinition error.
    }
    println(s)
}

Type Pattern

A type pattern is used to determine whether the runtime type of a value is a subtype of a specific type. There are two type patterns: _: Type (nesting a wildcard pattern _) and id: Type (nesting a binding pattern id). The difference is that the latter pattern will be bound to variables, while the former pattern will not.

For a value v to be matched and a type pattern id: Type (or _: Type), it is first determined whether a runtime type of v is a subtype of Type. If it is such subtype, the match is considered successful; otherwise, it is deemed a failure. If the match is successful, the type of v is converted to Type and bound to id (for _: Type, binding does not occur).

Assume that there are two classes: Base and Derived, where Derived is a subclass of Base. In the parameterless constructor of Base, a is set to 10. In the parameterless constructor of Derived, a is set to 20.

open class Base {
    var a: Int64
    public init() {
        a = 10
    }
}

class Derived <: Base {
    public init() {
        a = 20
    }
}

The following code shows the successful use of a type pattern:

main() {
    var d = Derived()
    var r = match (d) {
        case b: Base => b.a // Matched.
        case _ => 0
    }
    println("r = ${r}")
}

The result is as follows:

r = 20

The following code shows a failed match using a type pattern:

open class Base {
    var a: Int64
    public init() {
        a = 10
    }
}

class Derived <: Base {
    public init() {
        a = 20
    }
}

main() {
    var b = Base()
    var r = match (b) {
        case d: Derived => d.a // Type pattern match failed.
        case _ => 0 // Matched.
    }
    println("r = ${r}")
}

The result is as follows:

r = 0

Enum Pattern

Enum patterns are used to match instances of the enum type, defined similarly to the constructors of an enum: the C constructor without parameters or the C(p_1, p_2, ..., p_n) constructor with parameters, where the type prefix of the constructor can be omitted. The difference is that the p_1 to p_n (n is greater than or equal to 1) are patterns. For example, Some(1) is an enum pattern containing a constant pattern, and Some(x) is an enum pattern containing a binding pattern.

If an enum instance ev and an enum pattern ep are given, the ep can match the ev only when the constructor names of ev and ep are the same, and every value in the parameter list of ev matches the corresponding pattern in ep. For example, Some("one") can match only the Some constructor Option<String>.Some("one") of the Option<String> type, and Some(x) can match the Some constructor of any Option type.

The following example shows the use of the enum pattern. Because the constructor of x is Year, it matches the first case:

enum TimeUnit {
    | Year(UInt64)
    | Month(UInt64)
}

main() {
    let x = Year(2)
    let s = match (x) {
        case Year(n) => "x has ${n * 12} months" // Matched.
        case TimeUnit.Month(n) => "x has ${n} months"
    }
    println(s)
}

The result is as follows:

x has 24 months

Use | to connect multiple enum patterns.

enum TimeUnit {
    | Year(UInt64)
    | Month(UInt64)
}

main() {
    let x = Year(2)
    let s = match (x) {
        case Year(0) | Year(1) | Month(_) => "Ok" // Ok
        case Year(2) | Month(m) => "invalid" // Error, Variable cannot be introduced in patterns connected by '|'
        case Year(n: UInt64) | Month(n: UInt64) => "invalid" // Error, Variable cannot be introduced in patterns connected by '|'
    }
    println(s)
}

When the match expression is used to match enum values, the patterns after case must cover all constructors of the enum type to be matched. If they are not fully covered, the compiler will raise an error.

enum RGBColor {
    | Red | Green | Blue
}

main() {
    let c = Green
    let cs = match (c) { // Error, Not all constructors of RGBColor are covered.
        case Red => "Red"
        case Green => "Green"
    }
    println(cs)
}

To achieve full coverage, we can add case Blue, or we can use case _ at the end of the match expression to cover any unmatched cases.

enum RGBColor {
    | Red | Green | Blue
}

main() {
    let c = Blue
    let cs = match (c) {
        case Red => "Red"
        case Green => "Green"
        case _ => "Other" // Matched.
    }
    println(cs)
}

The execution result of the preceding code is as follows:

Other

Nested Combinations of Patterns

Tuple patterns and enum patterns can be nested with any patterns. The following code shows the use of different nested combinations:

enum TimeUnit {
    | Year(UInt64)
    | Month(UInt64)
}

enum Command {
    | SetTimeUnit(TimeUnit)
    | GetTimeUnit
    | Quit
}

main() {
    let command = SetTimeUnit(Year(2022))
    match (command) {
        case SetTimeUnit(Year(year)) => println("Set year ${year}")
        case SetTimeUnit(Month(month)) => println("Set month ${month}")
        case _ => ()
    }
}

The result is as follows:

Set year 2022