match Expression

Definition

In Cangjie, a match expression may contain or not contain a value to be matched.

A match expression with a value is as follows:

main() {
    let x = 0
    match (x) {
        case 1 => let r1 = "x = 1"
                  print(r1)
        case 0 => let r2 = "x = 0" // Matched.
                  print(r2)
        case _ => let r3 = "x != 1 and x != 0"
                  print(r3)
    }
}

The match expression starts with the keyword match, followed by the value to be matched (for example, x in the preceding example, which can be any expression), and then several case branches defined in a pair of braces.

Each case branch starts with the keyword case, which is followed by one or multiple patterns of the same type connected by | (1, 0, and _ in the preceding example are patterns. For details, see Pattern Overview). A pattern can be followed by an optional pattern guard, indicating additional conditions that must be satisfied after the case is successfully matched. Then, there is a =>, followed by the operations to be executed if this case branch matches. The operations can be a series of expressions, variables, and function definitions (the scope of a newly defined variable or function starts from the definition and ends before the next case), for example, the variable definitions and print function calls in the preceding example.

During execution, the match expression sequentially matches the expression following match with the patterns in each case. Once a match is found (if there is a pattern guard, the expression following where must also evaluate to true; if there are multiple patterns connected by |, matching against any one of them counts as a successful match), the code following => is executed, and the execution of the match expression exits (meaning it will not match against any subsequent cases). If no match is found, it continues to match against the patterns in the following cases until a match is successful (the match expression guarantees that there will always be a matching case).

In the above example, since the value of x is 0, it will match with the second case branch (here, the constant pattern is used to check whether the values are the same. For details, see Constant Pattern), resulting in the output: x = 0.

Compile and execute the preceding code. The output is as follows:

x = 0

The match expression requires that all matches be exhaustive, which means that all possible values of the expression to be matched must be considered. If the match expression is not exhaustive or the compiler cannot determine whether the expression is exhaustive, a compilation error is reported. In other words, the union set of the value ranges covered by all case branches (including pattern guards) should contain all possible values of the expression to be matched. A common way to ensure that the match expression is exhaustive is to use the wildcard pattern _ in the last case branch, because _ can match any value.

The exhaustiveness of the match expression ensures that there must be a case branch that matches the value. In the following example, an error is reported during compilation because all cases do not cover all possible values of x:

func nonExhaustive(x: Int64) {
    match (x) {
        case 0 => print("x = 0")
        case 1 => print("x = 1")
        case 2 => print("x = 2")
    }
}

After the patterns in the case branches, a pattern guard can be used to further evaluate the matched results. The pattern guard is represented by where cond. The expression cond must be of the Bool type.

In the following example (the enum pattern is used. For details, see Enum Pattern), when the parameter values of the RGBColor constructor are greater than or equal to 0, their values are printed. When the parameter values are less than 0, they are considered to be equal to 0.

enum RGBColor {
    | Red(Int16) | Green(Int16) | Blue(Int16)
}
main() {
    let c = RGBColor.Green(-100)
    let cs = match (c) {
        case Red(r) where r < 0 => "Red = 0"
        case Red(r) => "Red = ${r}"
        case Green(g) where g < 0 => "Green = 0" // Matched.
        case Green(g) => "Green = ${g}"
        case Blue(b) where b < 0 => "Blue = 0"
        case Blue(b) => "Blue = ${b}"
    }
    print(cs)
}

The result is as follows:

Green = 0

A match expression without a value is as follows:

main() {
    let x = -1
    match {
        case x > 0 => print("x > 0")
        case x < 0 => print("x < 0") // Matched.
        case _ => print("x = 0")
    }
}

In contrast to a match expression with a value, there is no expression to match after the match keyword, and the cases do not contain patterns but rather expressions of the Bool type (such as x > 0 and x < 0 in the code above) or _ (which represents true). Additionally, there are no pattern guards in the cases.

When a match expression without a value is executed, the values of the expressions following case are checked in sequence until a case branch whose value is true is found. If the value of the expression following a case is true, the code following the => in that case is executed, and the execution of the match expression is exited (meaning it will not evaluate any subsequent cases).

In the preceding example, since the value of x is -1, the value of the expression (x < 0) in the second case branch is true. In this case, print("x < 0") is executed.

Compile and execute the preceding code. The output is as follows:

x < 0

Types of the match Expression

For match expressions (regardless of whether there is a value):

  • When there is a clear type requirement in the context, the type of the code block following => in each case branch must be a subtype of the type required by the context.

  • When there is no explicit type requirement in the context, the type of the match expression is the minimum common parent type of the code blocks following => in each case branch.

  • When the value of the match expression is not used, its type is Unit, and there is no requirement for the branches to have the minimum common parent type.

Here are examples to illustrate these points.

let x = 2
let s: String = match (x) {
    case 0 => "x = 0"
    case 1 => "x = 1"
    case _ => "x != 0 and x != 1" // Matched.
}

In this example, the variable s is explicitly annotated with the type String, which is a case where the type information in the context is clear. Therefore, each case's code block following => must have a type that is a subtype of String. Clearly, the string literals after => in the above example satisfy this requirement.

Another example without context type information is as follows:

let x = 2
let s = match (x) {
    case 0 => "x = 0"
    case 1 => "x = 1"
    case _ => "x != 0 and x != 1" // Matched.
}

In this example, the variable s is not explicitly annotated with a type. However, since the type of the code blocks following => in each case is String, the type of the match expression is also String, which means the type of s can be determined to be String as well.