Skip to content

Commit

Permalink
doc: improvements
Browse files Browse the repository at this point in the history
- add match expression
- move around pattern matching related stuff
  • Loading branch information
peter-jerry-ye committed Dec 10, 2024
1 parent ae9f0d5 commit 9014de6
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 50 deletions.
135 changes: 91 additions & 44 deletions next/language/fundamentals.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@ Local functions can be named or anonymous. Type annotations can be omitted for l
:end-before: end local functions 1
```

There's also a form called **matrix function** that make use of [pattern matching](#pattern-matching):

```{literalinclude} /sources/language/src/functions/top.mbt
:language: moonbit
:start-after: start local functions 3
:end-before: end local functions 3
```

Functions, whether named or anonymous, are _lexical closures_: any identifiers without a local binding must refer to bindings from a surrounding lexical scope. For example:

```{literalinclude} /sources/language/src/functions/top.mbt
Expand Down Expand Up @@ -489,6 +497,57 @@ Note that a conditional expression always returns a value in MoonBit, and the re

The `else` clause can only be omitted if the return value has type `Unit`.

### Match Expression

The `match` expression is similar to conditional expression, but it uses [pattern matching](#pattern-matching) to decide which consequent to evaluate and extracting variables at the same time.

```{literalinclude} /sources/language/src/controls/top.mbt
:language: moonbit
:dedent:
:start-after: start match 1
:end-before: end match 1
```

If a possible condition is omitted, the compiler will issue a warning, and the program will terminate if that case were reached.

### Guard Statement

The `guard` statement is used to check a specified invariant.
If the condition of the invariant is satisfied, the program continues executing
the subsequent statements and returns. If the condition is not satisfied (i.e., false),
the code in the `else` block is executed and its evaluation result is returned (the subsequent statements are skipped).

```{literalinclude} /sources/language/src/controls/top.mbt
:language: moonbit
:dedent:
:start-after: start guard 1
:end-before: end guard 1
```

#### Guarded Let

The `let` statement can be used with [pattern matching](#pattern-matching). However, `let` statement can only handle one case. And `guard let` can solve this issue.

In the following example, `getProcessedText` assumes that the input `path` points to resources that are all plain text,
and it uses the `guard` statement to ensure this invariant. Compared to using
a `match` statement, the subsequent processing of `text` can have one less level of indentation.

```{literalinclude} /sources/language/src/controls/top.mbt
:language: moonbit
:start-after: start guard 2
:end-before: end guard 2
```

When the `else` part is omitted, the program terminates if the condition specified
in the `guard` statement is not true or cannot be matched.

```{literalinclude} /sources/language/src/controls/top.mbt
:language: moonbit
:dedent:
:start-after: start guard 3
:end-before: end guard 3
```

### While loop

In MoonBit, `while` loop can be used to execute a block of code repeatedly as long as a condition is true. The condition is evaluated before executing the block of code. The `while` loop is defined using the `while` keyword, followed by a condition and the loop body. The loop body is a sequence of statements. The loop body is executed as long as the condition is true.
Expand Down Expand Up @@ -689,41 +748,6 @@ A functional loop consumes arguments and returns a value. It is defined using th
:end-before: end for loop 9
```

### Guard Statement

The `guard` statement is used to check a specified invariant.
If the condition of the invariant is satisfied, the program continues executing
the subsequent statements and returns. If the condition is not satisfied (i.e., false),
the code in the `else` block is executed and its evaluation result is returned (the subsequent statements are skipped).

```{literalinclude} /sources/language/src/controls/top.mbt
:language: moonbit
:dedent:
:start-after: start guard 1
:end-before: end guard 1
```

The `guard` statement also supports pattern matching: in the following example,
`getProcessedText` assumes that the input `path` points to resources that are all plain text,
and it uses the `guard` statement to ensure this invariant. Compared to using
a `match` statement, the subsequent processing of `text` can have one less level of indentation.

```{literalinclude} /sources/language/src/controls/top.mbt
:language: moonbit
:start-after: start guard 2
:end-before: end guard 2
```

When the `else` part is omitted, the program terminates if the condition specified
in the `guard` statement is not true or cannot be matched.

```{literalinclude} /sources/language/src/controls/top.mbt
:language: moonbit
:dedent:
:start-after: start guard 3
:end-before: end guard 3
```

## Iterator

An iterator is an object that traverse through a sequence while providing access
Expand Down Expand Up @@ -1043,22 +1067,39 @@ The type alias can be removed after all uses of `@pkgA.T` is migrated to `@pkgB.

## Pattern Matching

We have shown a use case of pattern matching for enums, but pattern matching is not restricted to enums. For example, we can also match expressions against Boolean values, numbers, characters, strings, tuples, arrays, and struct literals. Since there is only one case for those types other than enums, we can pattern match them using `let` binding instead of `match` expressions. Note that the scope of bound variables in `match` is limited to the case where the variable is introduced, while `let` binding will introduce every variable to the current scope. Furthermore, we can use underscores `_` as wildcards for the values we don't care about, use `..` to ignore remaining fields of struct or elements of array.
Pattern matching allows us to match on specific pattern and bind data from data structures.

### Simple Patterns

We can pattern match expressions against

- literals, such as boolean values, numbers, chars, strings, etc
- constants
- structs
- enums
- arrays
- maps
- JSONs

and so on. We can define identifiers to bind the matched values so that they can be used later.

```{literalinclude} /sources/language/src/pattern/top.mbt
:language: moonbit
:dedent:
:start-after: start pattern 1
:end-before: end pattern 1
:start-after: start simple pattern 1
:end-before: end simple pattern 1
```

We can use `_` as wildcards for the values we don't care about, and use `..` to ignore remaining fields of struct or enum, or array (see [array pattern](#array-pattern)).

```{literalinclude} /sources/language/src/pattern/top.mbt
:language: moonbit
:start-after: start pattern 2
:end-before: end pattern 2
:dedent:
:start-after: start simple pattern 2
:end-before: end simple pattern 2
```

There are some other useful constructs in pattern matching. For example, we can use `as` to give a name to some pattern, and we can use `|` to match several cases at once. A variable name can only be bound once in a single pattern, and the same set of variables should be bound on both sides of `|` patterns.
We can use `as` to give a name to some pattern, and we can use `|` to match several cases at once. A variable name can only be bound once in a single pattern, and the same set of variables should be bound on both sides of `|` patterns.

```{literalinclude} /sources/language/src/pattern/top.mbt
:language: moonbit
Expand All @@ -1078,6 +1119,12 @@ Array pattern have the following forms:
- `[pa, ..]` : matching for known number of elements, followed by unknown number of elements
- `[.., pa]` : matching for known number of elements, preceded by unknown number of elements

```{literalinclude} /sources/language/src/pattern/top.mbt
:language: moonbit
:start-after: start pattern 2
:end-before: end pattern 2
```

### Range Pattern
For builtin integer types and `Char`, MoonBit allows matching whether the value falls in a specific range.

Expand Down Expand Up @@ -1110,14 +1157,14 @@ The `key? : value` syntax will match no matter `key` exists or not, and `value`
:end-before: end pattern 5
```

- To match a data type `T` using map pattern, `T` must have a method `op_get(Self, K) -> Option[V]` for some type `K` and `V`.
- Currently, the key part of map pattern must be a constant
- To match a data type `T` using map pattern, `T` must have a method `op_get(Self, K) -> Option[V]` for some type `K` and `V` (see [method and trait](./methods.md)).
- Currently, the key part of map pattern must be a literal or constant
- Map patterns are always open: unmatched keys are silently ignored
- Map pattern will be compiled to efficient code: every key will be fetched at most once

### Json Pattern

When the matched value has type `Json`, literal patterns can be used directly:
When the matched value has type `Json`, literal patterns can be used directly, together with constructors:

```{literalinclude} /sources/language/src/pattern/top.mbt
:language: moonbit
Expand Down
29 changes: 23 additions & 6 deletions next/sources/language/src/controls/top.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,17 @@ test {
}
// end for loop 10

fn f() -> Unit {
let index = 1
let len = 10
// start guard 1
guard index >= 0 && index < len else { abort("Index out of range") }
// end guard 1
// start guard 1
fn guarded_get(array : Array[Int], index : Int) -> Int? {
guard index >= 0 && index < array.length() else { None }
Some(array[index])
}

test {
inspect!(guarded_get([1, 2, 3], -1), content="None")
}
// end guard 1

fn process(string : String) -> String {
string
}
Expand Down Expand Up @@ -261,3 +264,17 @@ fn g() -> Unit {
// <=> guard let Some(x) = expr else { _ => panic() }
// end guard 3
}

// start match 1
fn decide_sport(weather : String, humidity : Int) -> String {
match weather {
"sunny" => "tennis"
"rainy" => if humidity > 80 { "swimming" } else { "football" }
_ => "unknown"
}
}

test {
assert_eq!(decide_sport("sunny", 0), "tennis")
}
// end match 1
7 changes: 7 additions & 0 deletions next/sources/language/src/functions/top.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ test {
}
// end local functions 2

// start local functions 3
let extract : (Int?, Int) -> Int = fn {
Some(x), _ => x
None, default => default
}
// end local functions 3

// start labelled arguments 1
fn labelled_1(arg1~ : Int, arg2~ : Int) -> Int {
arg1 + arg2
Expand Down
42 changes: 42 additions & 0 deletions next/sources/language/src/pattern/top.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ enum Arith {
fn eval(expr : Arith) -> Int {
// start pattern 3
match expr {
//! Add(e1, e2) | Lit(e1) => ...
Lit(n) as a => ...
Add(e1, e2) | Mul(e1, e2) => ...
_ => ...
Expand Down Expand Up @@ -86,7 +87,48 @@ fn json() -> Unit {
// start pattern 6
match json {
{ "version": "1.0.0", "import": [..] as imports } => ...
{ "version": Number(i), "import": Array(imports)} => ...
_ => ...
}
// end pattern 6
}

// start simple pattern 1
const ONE = 1

fn match_int(x : Int) -> Unit {
match x {
0 => println("zero")
ONE => println("one")
value => println(value)
}
}
// end simple pattern 1

// start simple pattern 2
struct Point3D {
x : Int
y : Int
z : Int
}

fn match_point3D(p : Point3D) -> Unit {
match p {
{ x: 0, .. } => println("on yz-plane")
_ => println("not on yz-plane")
}
}

enum Point[T] {
Point2D(Int, Int, name~: String, payload~ : T)
}

fn match_point[T](p : Point[T]) -> Unit {
match p {
//! Point2D(0, 0) => println("2D origin")
Point2D(0, 0, ..) => println("2D origin")
Point2D(_) => println("2D point")
_ => panic()
}
}
// end simple pattern 2

0 comments on commit 9014de6

Please sign in to comment.