Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Chapter14 #45

Merged
merged 8 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
ot = "ot"

[type.po]
extend-glob = ["*.webp"]
extend-glob = ["*.svg", "*.webp"]
check-file = false
2 changes: 1 addition & 1 deletion docs/00-course-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The course is suitable for a wide audience, from programming beginners to experi
| 11 | [Case Study: Parser](./parser) | Pending | Pending | |
| 12 | [Case Study: Autodiff](./autodiff) | Pending | Pending | |
| 13 | [Case Study: Neural Network](./neural-network) | Pending | Pending | |
| 14 | Case Study: Stack Machine | Pending | Pending | |
| 14 | [Case Study: Stack Machine](./stack-machine) | Pending | Pending | |

Chapters 1 to 6 focus on functional programming, where we will learn about basic data structures and algorithms, and then delve into advanced topics such as higher-order functions and interfaces. Starting from Chapter 7, we will move on to imperative programming, where we will work with mutable data. Finally, we will conclude our study with an introduction to object-oriented programming. In the last few chapters, we will present some case studies to demonstrate how to use MoonBit to develop complex programs.

Expand Down
4 changes: 2 additions & 2 deletions docs/07-imperative-programming.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ The distinction between mutable and immutable data is important because it affec
- In the second diagram, we use `let` to bind the identifier `ref` to a struct. Thus, the box contains a reference to the struct. When we modify the value in the struct using `ref`, we are updating the value stored in the struct which it points to. The reference itself does not change because it still points to the same struct.
- In the third diagram, when we define a mutable `ref` and modify it, we are creating a new box and updating the reference to point to the new box.

![](/pics/ref.drawio.svg)
![](/pics/ref.drawio.webp)

### Aliases

Expand All @@ -115,7 +115,7 @@ fn init {
}
```

![](/pics/alias.drawio.svg)
![](/pics/alias.drawio.webp)

## Loops

Expand Down
8 changes: 4 additions & 4 deletions docs/08-queues.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ When implementing a circular queue, we keep track of the `start` and `end` indic

The following diagram is a demonstration of these operations. First, we create an empty queue by calling `make()`. At this point, the `start` and `end` indices both point to the first element. Then, we push an element into the queue by calling `push(1)`. The element `1` is then added to the position pointed to by `end`, and the `end` index is updated. Afterwards, we call `push(2)` to push another element into the queue, and finally call `pop()` to pop out the first element we have pushed.

![](/pics/circle_list.drawio.svg)
![](/pics/circle_list.drawio.webp)

Now, let's take a look at another situation when we are close to the end of the array. At this point, `end` points to the position of the last element of the array. When we push an element, `end` cannot move forward, so we wrap it back to the beginning of the array. Then, we perform two `pop` operations. Similarly, when `start` exceeds the length of the array, it returns to the beginning of the list.

![](/pics/circle_list_back.drawio.svg)
![](/pics/circle_list_back.drawio.webp)

With the above basic ideas in mind, we can easily define the `Queue` struct and implement the `push` operation as follows:

Expand Down Expand Up @@ -180,9 +180,9 @@ fn push[T](self: LinkedList[T], value: T) -> LinkedList[T] {

The following diagram is a simple demonstration. When we create a linked list by calling `make()`, both the `head` and `tail` are empty. When we push an element using `push(1)`, we create a new node and point both the `head` and `tail` to this node. When we push more elements, say `push(2)` and then `push(3)`, we need to update the `next` field of the current `tail` node to point to the new node. The `tail` node of the linked list should always point to the latest node.

![](/pics/linked_list.drawio.svg)
![](/pics/linked_list.drawio.webp)

![](/pics/linked_list_2.drawio.svg)
![](/pics/linked_list_2.drawio.webp)

To get the length of the list, we can either record it in the struct as we did for circular queues, or we can use a naive recursive function to calculate it.

Expand Down
12 changes: 6 additions & 6 deletions docs/10-hash-maps-closures.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Another approach is **open addressing** which does not change the type of the ar

Let's start with direct addressing. When a hash/index collision occurs, store the data of the same index into some data structure like lists. For example, when adding 0 and 5 (with hash values of 0 and 5 respectively) into an array of length 5, both 0 and 5 mod length 5 will be 0 and are added into a list at index 0:

![](/pics/separate_chaining.drawio.svg)
![](/pics/separate_chaining.drawio.webp)

For implementation, we'll define two additional data structures: 1) a key-value pair that enables convenient in-place value updates; and 2) a mutable list where a null value means an empty list, and otherwise the tuples are the head element plus the remaining list. Finally, let's define the hash map. It contains an array of lists of key-value pairs, and we dynamically maintain the length of the array and the number of key-value pairs.

Expand All @@ -67,7 +67,7 @@ struct HT_bucket[K, V] {

For the add/update operation, we first calculate the position to store the key based on its hash value. Then, we look up if the key already exists by traversing the list. If the key exists, we update the value, and if not we add the key-value pair. Similarly, we check the corresponding list and update it for the remove operation.

![height:200px](/pics/separate_chaining_op_en.drawio.svg)
![height:200px](/pics/separate_chaining_op_en.drawio.webp)

The following code demonstrates adding and updating data. We first calculate the hash value of the key at line 2 with the hash interface specified in `K : Hash` at line 1. Then we find and traverse the corresponding data structure. We're using a mutable data structure with an infinite while loop at line 4. We break out of the loop if we find the key already exists or reach the end of the list. If the key is found, we update the data in place. Otherwise, we update the bucket to be the remaining list so the loop terminates. When we reach the end of the list and haven't found the key, we add a new pair of data at the end of the list. At last, we check if it needs resizing based on the current load factor.

Expand Down Expand Up @@ -124,7 +124,7 @@ fn remove[K : Hash + Eq, V](map : HT_bucket[K, V], key : K) -> Unit {

Let's continue with open addressing. Recall that linear probing is when a hash collision occurs, we keep incrementing the index to find the next empty slot to place the collided key. In the following example, we first add 0 whose hash value is 0 into slot 0. Then, we add 1 whose hash value is 1 into slot 1. Lastly, we add 5 whose hash value is 5, but it exceeds the range of indices and we use modulo to get 0 instead. In theory, we should store 5 in slot 0, but the slot was already taken. Therefore, we increment the index until we find the next empty slot which is slot 2 as slot 1 was also taken. Note that an invariant must be maintained throughout the program: there should be no empty slots between the original slot and the slot where the key-value pair is actually stored. This ensures we won't waste time traversing the whole hash map to check if some key-value pair already exists or not. Also, since we made sure there's no gap between the slots, we can exit the loop once the next empty slot is found.

![height:300px](/pics/open_address_en.drawio.svg)
![height:300px](/pics/open_address_en.drawio.webp)

To implement open addressing, we will use an array with default values similar to the implementation of a circular queue introduced in the last lecture. Feel free to try and implement it using Option as well. Besides the array to store key-value pairs, we also have an array of boolean values to determine if the current slot is empty. As usual, we dynamically maintain the length of the array and the number of key-value pairs.

Expand Down Expand Up @@ -184,7 +184,7 @@ The remove operation is more complicated. Recall that we have an invariant to ma

A simple solution is to define a special state that marks a slot as "deleted" to ensure subsequent data can still be reached and found. Another solution is to check if any element from the slot of data removal to the next empty slot needs to move location so as to maintain the invariant. Here we demonstrate the simpler marking method, also known as "tombstone".

![height:320px](/pics/open_address_delete_en.drawio.svg)
![height:320px](/pics/open_address_delete_en.drawio.webp)

We define a new `Status` enum consisting of `Empty`, `Occupied` and `Deleted`, and update the type of the occupied array from boolean value to Status.

Expand Down Expand Up @@ -241,15 +241,15 @@ Next, let's introduce another implementation of open addressing: rearrange eleme

First, we check element 5 and notice that 5 should be mapped to index 0, but is stored in the current slot to handle hash collision. Now that element 1 has been removed, the invariant no longer holds as there's an empty slot between indices 0 and 2. To solve this, we need to move element 5 forward to the index previously storing element 1. Then we check element 3 and it's in the slot it should be mapped to, so we do not move it. We encounter an empty slot after element 3. The elements after the empty spot won't be affected, so we stop checking.

![](/pics/rearrange_en.drawio.svg)
![](/pics/rearrange_en.drawio.webp)

Let's look at another example as follows: we have an array of size 10, so a number that ends in *n* will be mapped to index *n* with modulo, like the index for element 0 is 0, for element 11 is 1, for element 13 is 3, etc. We will remove the data at index 1 and rearrange the elements in the hash map. We check the elements at index 1 to 5 and:

We find element 11 should be stored at index 1 if there were no hash collision. After removing the data at index 1, we now have an empty slot at index 1 and can move element 11 to it. Then we check element 3 and it's already in the slot it should be mapped to. Next, we check element 21 which should be stored at index 1, but now we see a gap between slot 1 to the actual slot element 21 is stored. This is caused by moving element 11 earlier, so also move element 21 forward. Lastly, we check element 13 which should be stored at index 3. Now there's a gap after moving element 21, so we move element 13 forward as well.

Now, the invariant holds again: there should be no empty slots between the original slot and the slot where the key-value pair is actually stored. The detailed implementation is left as an exercise and feel free to give it a try!

![](/pics/rearrange_2_en.drawio.svg)
![](/pics/rearrange_2_en.drawio.webp)

## Closure

Expand Down
4 changes: 2 additions & 2 deletions docs/11-parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Whitespace = " "

Let's take integers and the plus sign for examples. Each line in the lexical rules corresponds to a pattern-matching rule. Content within quotes means matching a string of the same content. Rule `a b` means matching rule `a` first, and if it succeeds, continue to pattern match rule `b`. Rule `a / b` means matching rule `a` or `b`, try matching `a` first, and then try matching rule `b` if it fails. Rule `*a ` with an asterisk in front refers to zero or more matches. Lastly, `%x` means matching a UTF-encoded character, where `x` indicates it's in hexadecimal. For example, `0x30` corresponds to the 48th character `0`, and it is `30` in hexadecimal. With this understanding, let's examine the definition rules. Plus is straightforward, representing the plus sign. Number corresponds to zero or a character from 1-9 followed by zero or more characters from 0-9.

![](/pics/lex_rail.drawio.svg)
![](/pics/lex_rail.drawio.webp)

In MoonBit, we define tokens as enums, and tokens can be values containing integers or operators and parentheses. Whitespaces are simply discarded.

Expand Down Expand Up @@ -183,7 +183,7 @@ expression =/ expression "+" expression / expression "-" expression
expression =/ expression "*" expression / expression "/" expression
```

![](/pics/ast-example.drawio.svg)
![](/pics/ast-example.drawio.webp)

However, our syntax rules have some issues since it doesn't differentiate the precedence levels. For instance, `a + b * c` should be interpreted as `a` plus the product of `b` and `c`, but according to the current syntax rules, the sum of `a` and `b` multiplied by `c` is also valid, which introduces ambiguity. It also doesn't show associativity. Arithmetic operators should be left-associative, meaning `a + b + c` should be interpreted as `a` plus `b`, then adding `c`. However, the current syntax also allows adding `a` to the sum of `b` and `c`. So, we need to adjust the syntax rules for layering.

Expand Down
2 changes: 1 addition & 1 deletion docs/13-neural-network.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Machine learning is a vast and complex field, and the knowledge related to neura

Neural networks are a subtype of machine learning. As the name suggests, they simulate the neural structure of the human brain. A classic neural network usually consists of multiple layers and interconnected neurons. A neuron typically has multiple inputs, a single output, and weights to calculate the output signal based on the input signals. A neuron typically activates once it reaches a certain threshold.

![](/pics/neural_network.drawio.svg)
![](/pics/neural_network.drawio.webp)

The above diagram an illustration of a neural network. Each node represents a neuron. The yellow neuron receives inputs from the four neurons in the left layer and passes its output to the neurons in the right layer.

Expand Down
Loading