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 theRune
literal and the single-character string literal can be used as constant patterns representingRune
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 theByte
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