Skip to content

Commit

Permalink
update Chapter 5
Browse files Browse the repository at this point in the history
  • Loading branch information
skylee03 committed Jun 3, 2024
1 parent 77054dc commit f85c206
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 16 deletions.
2 changes: 2 additions & 0 deletions docs/02-development-environments-expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ In MoonBit, the type for Boolean values is `Bool`, and it can only have two poss

In MoonBit, `==` represents a comparison between values. In the above examples, the left-hand side is an expression, and the right-hand side is the expected result. In other words, these examples themselves are expressions of type `Bool`, and we expect their values ​​to be `true`.

The `||` and `&&` operators are short-circuited. This means that if the outcome of the entire expression can be determined, the calculation will be halted, and the result will be immediately returned. For instance, in the case of `true || ...`, it is evident that `true || any value` will always yield true. Therefore, only the left side of the `||` operator needs to be evaluated. Similarly, when evaluating `false && ...`, since it is known that `false && any value` will always be false, the right side is not evaluated either. In this case, if the right side of the operator contains side effects, those side effects may not occur.

Quiz: How to define XOR (true if only one is true) using OR, AND, and NOT?

#### Integers
Expand Down
52 changes: 45 additions & 7 deletions docs/03-functions-lists-recursion.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
```moonbit
let pi = 3.1415
fn put(map: @map.Map[Int, Int64], num: Int, result: Int64) -> @map.Map[Int, Int64] {
fn put(map: @sorted_map.Map[Int, Int64], num: Int, result: Int64) -> @sorted_map.Map[Int, Int64] {
map.insert(num, result)
}
fn get(map: @map.Map[Int, Int64], num: Int) -> Option[Int64] {
fn get(map: @sorted_map.Map[Int, Int64], num: Int) -> Option[Int64] {
map.lookup(num)
}
fn make() -> @map.Map[Int, Int64] {
@map.empty()
fn make() -> @sorted_map.Map[Int, Int64] {
@sorted_map.empty()
}
```
Expand Down Expand Up @@ -165,6 +165,44 @@ For example:

Since `->` is right-associative, in the last example, the brackets in the return type can be omitted.

### Labeled Arguments and Optional Arguments

It is not uncommon to encounter difficulties recalling the order of parameters when calling a function, particularly when multiple parameters share the same type. In such situations, referring to documentation or IDE prompts can be helpful. However, when reviewing code written by others, these resources may not be readily available. To overcome this challenge, labeled arguments offer a practical solution. In MoonBit, we can make a parameter "labeled" by prefixing it with `~`. For instance, consider the following code snippet:

```moonbit
fn greeting1(~name: String, ~location: String) -> Unit {
println("Hi, \(name) from \(location)!")
}
fn init {
greeting1(~name="somebody", ~location="some city")
let name = "someone else"
let location = "another city"
// `~label=label` can be abbreviated as `~label`
greeting1(~name, ~location)
}
```

By using labeled arguments, the order of the parameters becomes less important. In addition, they can be made optional by specifying a default value when declaring them. When the function is called, if no argument is explicitly provided, the default value will be used.

Consider the following example:

```moonbit
fn greeting2(~name: String, ~location: Option[String] = None) -> Unit {
match location {
Some(location) => println("Hi, \(name)!")
None => println("Hi, \(name) from \(location)!")
}
}
fn init {
greeting2(~name="A") // Hi, A!
greeting2(~name="B", ~location=Some("X") // Hi, B from X!
}
```

It is important to note that the default value expression will be evaluated each time the function is called.

## Lists

Data is everywhere. Sometimes, we have data with the following characteristics:
Expand Down Expand Up @@ -555,13 +593,13 @@ fn get(map: IntMap, num: Int) -> Option[Int64] // Retrieve

In other words, we should be able to perform the following operations using an `IntMap`: create an empty map, insert a key-value pair into it, and look up the value corresponding to a given key.

Thanks to our paradigm of modular programming, we only need to care about the interfaces rather than the specific implementation. Therefore, there are many siutable data structures in MoonBit's standard library. In this example, we will use `@map.Map[Int, Int64]`, but we can easily replace it with another data structure, as long as it implements the interfaces we need.
Thanks to our paradigm of modular programming, we only need to care about the interfaces rather than the specific implementation. Therefore, there are many siutable data structures in MoonBit's standard library. In this example, we will use `@sorted_map.Map[Int, Int64]`, but we can easily replace it with another data structure, as long as it implements the interfaces we need.

In the top-down implementation, before each computation, we first check if our desired result has been cached: if it does, we can simply use the result; if it doesn't, we calculate the result and store it in the data structure.

```moonbit expr
fn fib1(num: Int) -> Int64 {
fn aux(num: Int, map: @map.Map[Int, Int64]) -> (Int64, @map.Map[Int, Int64]) {
fn aux(num: Int, map: @sorted_map.Map[Int, Int64]) -> (Int64, @sorted_map.Map[Int, Int64]) {
match get(map, num) {
Some(result) => (result, map)
None => {
Expand Down Expand Up @@ -608,7 +646,7 @@ In the bottom-up implementation, we typically start from the smallest subproblem

```moonbit expr
fn fib2(num: Int) -> Int64 {
fn aux(n: Int, map: @map.Map[Int, Int64]) -> Int64 {
fn aux(n: Int, map: @sorted_map.Map[Int, Int64]) -> Int64 {
let result = get_or_else(get(map, n - 1), 1L) +
get_or_else(get(map, n - 2), 1L)
if n == num { result }
Expand Down
29 changes: 29 additions & 0 deletions docs/04-tuples-structs-enums.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,35 @@ enum ComputeResult {

To do this, simply enclose parameters with parentheses and separate them by commas after each variant. In the second example, we define the case of successful integer operation, and the value is an integer. Enumerated types correspond to a distinguishable union. What does that mean? First, it is a union of different cases, for example, the set represented by the type `T` for `Some` and the set defined by the singular value `None`. Second, this union is distinguishable because each case has a unique name. Even if there are two cases with the same data type, they are entirely different. Thus, enumerated types are also known as sum types.

### Labeled Arguments

Similar to functions, enum constructors also support the use of labeled arguments. This feature is beneficial in simplifying pattern matching patterns. For example:

```moonbit
enum Tree[X] {
Nil
Branch(X, ~left : Tree[X], ~right : Tree[X])
}
fn leftmost[X](self : Tree[X]) -> Option[X] {
loop self {
Nil => None
// use `label=pattern` to match labeled arguments of constructor
Branch(top, left=Nil, right=Nil) => Some(top)
// `label=label` can be abbreviated as `~label`
Branch(_, left=Nil, ~right) => continue right
// use `..` to ignore all remaining labeled arguments
Branch(_, ~left, ..) => continue left
}
}
fn init {
// syntax for creating constructor with labeled arguments is the same as callig labeled function
let t: Tree[Int] = Branch(0, right=Nil, left=Branch(1, left=Nil, right=Nil))
println(t.leftmost()) // `Some(1)`
}
```

## Algebraic Data Types

We've mentioned product types and sum types. Now, let me briefly introduce algebraic data types. It's important to note that this introduction to algebraic data types is quite basic. Please read the references for a deeper understanding.
Expand Down
17 changes: 8 additions & 9 deletions docs/05-trees.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ fn dfs_search(target: Int, tree: IntTree) -> Bool {

As we introduced earlier, this is a traversal based on structural recursion. We first handle the base case, i.e., when the tree is empty, as shown in the third line. In this case, we haven't found the value we're looking for, so we return `false`. Then, we handle the recursive case. For a node, we check if its value is the desired result, as shown in line 5. If we find it, the result is `true`. Otherwise, we continue to traverse the left and right subtrees alternately, if either we find it in left subtree or right subtree will the result be `true`. In the current binary tree, we need to traverse both the left and right subtrees to find the given value. The binary search tree introduced later will optimize this process. The only differences between preorder, inorder, and postorder searches is the order of operations on the current node, the left subtree search, and the right subtree search.

### Short-Circuit Evaluation of Booleans

Here, we supplement the content not mentioned in the second lesson, the short-circuit evaluation of booleans. The logical `OR` and `AND` operations are short-circuited. This means that if the result of the current evaluation can be determined, the calculation will be terminated, and the result will be returned directly. For example, in the first case, we are evaluating `true ||` some value. It's clear that `true || any value` will always be true, so only the left side of the operation needs to be evaluated, and the right side won't be evaluated. Therefore, even if we write an instruction such as `abort` here to halt the program, the program will still run normally because the right side hasn't been computed at all. Similarly, if we evaluate `false &&` some value, knowing that `false && any value` will always be false, we won't evaluate the right side either. This brings us back to the traversal of the tree we discussed earlier. When using logical OR, traversal will immediately terminate once any condition is met.

### Queues

Now let's continue with breadth-first traversal.
Expand Down Expand Up @@ -299,13 +295,13 @@ The key to maintaining balance in a binary balanced tree is that when the tree b
enum AVLTree {
Empty
// current value, left subtree, right subtree, height
Node(Int, AVLTree, AVLTree, Int)
Node(Int, ~left: AVLTree, ~right: AVLTree, ~height: Int)
}
fn create(value: Int, left: AVLTree, right: AVLTree) -> AVLTree
fn create(value : Int, ~left : AVLTree = Empty, ~right : AVLTree = Empty) -> AVLTree
fn height(tree: AVLTree) -> Int
```

The creation function creates a new AVL tree without explicitly maintaining its height. Since the insertion and deletion operations of AVL trees are similar to standard binary search trees, we won't go into detail here.
Here, we used the syntax for labeled arguments, which we introduced in [Chapter 3](./functions-lists-recursion#labeled-argument-and-optional-arguments) and [Chapter 4](./tuples-structs-enums#labeled-arguments). The `create` function creates a new AVL tree whose both subtrees are empty by default, without explicitly maintaining its height. Since the insertion and deletion operations of AVL trees are similar to standard binary search trees, we won't go into detail here.

![](/pics/rotation.drawio.webp)

Expand All @@ -332,8 +328,11 @@ Here is a snippet of code for a balanced tree. You can easily complete the code
```moonbit no-check
fn add(tree: AVLTree, value: Int) -> AVLTree {
match tree {
Node(v, left, right, _) as t => {
if value < v { balance(add(left, value), v, right) } else { ... }
// When encountering the pattern `Node(v, ..) as t`,
// the compiler will know that `t` must be constructed by `Node`,
// so `t.left` and `t.right` can be directly accessed within the branch.
Node(v, ..) as t => {
if value < v { balance(add(t.left, value), v, t.right) } else { ... }
}
Empty => ...
}
Expand Down

0 comments on commit f85c206

Please sign in to comment.