Skip to content

Commit

Permalink
feat: update Chapter 7
Browse files Browse the repository at this point in the history
  • Loading branch information
skylee03 committed Jun 6, 2024
1 parent 47b1dcb commit a7c4ca8
Showing 1 changed file with 79 additions and 38 deletions.
117 changes: 79 additions & 38 deletions docs/07-imperative-programming.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,107 @@
# 7. Imperative Programming

## Revisiting Functional Programming
## Functional vs Imperative Programming

Imperative programming is a style of programming where we give the computer a set of commands to execute, much like giving instructions to a person. It's about telling the computer exactly what to do, step by step. This is different from functional programming, where you define what you want in terms of mathematical functions. In functional programming, the same input will always give you the same output, and there's a concept called "referential transparency," which means you can replace a function call with its result without changing the program's behavior.
In the previous chapters, we used the functional programming paradigm, where the same input always produces the same output. Functional programs typically exhibit the property of referential transparency. That is, we can replace a function call with its result without changing the program's behavior.

However, in the real world, we often need our programs to do more than just calculations. We might need them to read from a file, write to the screen, or change data in memory. These actions are called "side effects," and they can make our programs behave differently each time they run, even with the same input. This breaks the referential transparency, making our programs harder to understand and predict.
Consider the example below. We can declare a variable `x` and assign it the result of an expression, like `1 + 1`. This is straightforward and mirrors what we do in mathematics. We can also define a function `square` that takes an integer and returns its square. When we call `square(x)`, we get the result `4`, just like if we replaced `square(x)` with `{ 2 * 2 }`.

```moonbit expr
let x: Int = 1 + 1 // x can be directly replaced with 2
fn square(x: Int) -> Int { x * x };
let z: Int = square(x) // can be replaced with { 2 * 2 }, still resulting in 4
```

Let's look at the example here in MoonBit. We can declare a variable `x` and assign it the result of an expression, like `1 + 1`. This is straightforward and mirrors what we do in mathematics. We can also define a function `square` that takes an integer and returns its square. When we call `square(x)`, we get the result `4`, just like if we replaced `square(x)` with `{ 2 * 2 }`.
However, in the real world, we often need our programs to do more than just calculations. We might need them to read from a file, write to the screen, or change data in memory. These actions are called "side effects," and they can make our programs behave differently each time they run, even with the same input. This breaks the referential transparency, making our programs harder to understand and predict.

In this chapter, we will learn the basic concepts of imperative programming. It is a paradigm of programming where we give the computer a set of commands to execute, much like giving instructions to a person. It's about telling the computer exactly what to do, step by step. This is different from functional programming, where you define what you want in terms of mathematical functions.

## Commands and Side Effects

In MoonBit, we can use the `print` command to output text to the screen. This is a side effect because it changes the state of the world outside our program. When we define a function, we usually want it to return a value, but sometimes we just want to perform an action without a return value. In such cases, we use the `Unit` type, which represents the absence of a meaningful return value, like a placeholder.
In MoonBit, one of the most used commands is `println`, which outputs its arguments to the screen with a new line at the end. The action of "printing" is a side effect because it changes the state of the world outside the program. Next, we will explore how side effects challenge the computational model we established in [Chapter 2](./development-environments-expressions).

In the following example, the value assigned to `x` comes from an expression block, in which the `println` command is executed first, and then the result of the expression `1 + 1` is assigned to `x`. Finally, the value of `z` is the result of `square(x)`, which is `4`. In the whole process, `println` is executed only once.

```moonbit
fn square(x: Int) -> Int { x * x }
fn init {
let x: Int = {
println("hello moonbit") // First, execute the command and perform output
1 + 1 // Then, take the last value of the expression block as the block's value
println("hello moonbit") // Print once
1 + 1 // 2
}
let z: Int = square(x) // Output once
let z: Int = square(x) // 4
}
```

![](/pics/print_once.webp)

However, if we replace all occurrences of `x` and `square` with their definitions using the method introduced in [Chapter 2](./development-environments-expressions), we will get the following program:

```moonbit
fn init {
let z: Int = {
println("hello moonbit");
1 + 1
println("hello moonbit"); // Print once
1 + 1 // 2
} * {
println("hello moonbit");
1 + 1
} // return value is also 4, but output twice, which means the block's effect is not equal to x
println("hello moonbit"); // Print twice
1 + 1 // 2
} // 4
}
```

Here's a simple example: we define a function `init`, a function that will be executed at the initialization stage of the program, that prints a greeting message and then calculates the value of `x`. The `println` command is executed first, and then the result of the expression `1 + 1` is used as the value of `x`. This function has a side effect (printing), but it also returns a value (the value of `x`), which is why we use the `Unit` type to indicate that the primary purpose of this function is not to return a value.
![](/pics/print_twice.webp)

In functional programming, the two programs should be equivalent, because they compute the same result. However, because of the side effect of `println`, the two programs behave differently: the former produces one line of output, while the latter produces two lines of output. It can be seen that side effects could destroy referential transparency and thus make it more difficult to reason about programs.

### The Unit Type

In [Chapter 2](./development-environments-expressions#unit), we introduced the `Unit` type, but we did not clearly explain its usage at the time. Here, we can see that in imperative programming, commands like `println` are purely for side effects and do not have a return value. However, in MoonBit, commands are just a special sort of expression. Besides performing their side effects, they should also be able to be reduced to a value, which of course needs to have a certain type. In such cases, we use the `Unit` type, which consists of only one value `()` and is thus suitable for representing the absence of a meaningful return value, like a placeholder.

## Variables and Aliases
In particular, the `let` statement in MoonBit is essentially a command, and its type is also `Unit`. For example:

```moonbit
fn do_nothing() -> Unit {
let _x = 0 // The `let` statement is of type `Unit`
}
```

## Mutable Variables

As we learned in [Chapter 3](./functions-lists-recursion), we can create mutable variables in MoonBit using `let mut`, and update the binding with the command `<variable> = <expression>`, whose type is also `Unit`. For example:

```moonbit
fn init {
let mut x = 1
x = 10 // The assignment operation is a command.
}
```

In MoonBit, we can create mutable variables using `let mut`. The `mut` keyword tells MoonBit that the variable can change. This is important because it affects how we think about the data. For example, if we have a struct (a collection of values) called `Ref`, we can create a mutable reference with an integer inside it. We can then change the value of that integer, and any other references to the same `Ref` struct will see the updated value. This is because they are aliases, or different names for the same data.
In [Chapter 4](./tuples-structs-enums), we learned about structs. In MoonBit, the fields of a struct are immutable by default, but mutable fields are also supported. To make a filed mutable, we need to mark it with `mut`. For example:

```moonbit
struct Ref[T] { mut val : T }
fn init {
let ref: Ref[Int] = { val: 1 } // ref itself is just a data binding
let ref: Ref[Int] = { val: 1 } // `ref` itself is just a data binding
ref.val = 10 // We can modify the fields of the struct
println(ref.val.to_string()) // Output 10
}
```

The distinction between mutable and immutable data is important because it affects how we think about the data. As shown in the following diagrams, for mutable data, we can think of identifiers as boxes that hold values.
- In the first diagram, when we modify a mutable variable, we are essentially updating the value stored in the box.
- 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)

### Aliases

Multiple identifiers pointing to the same mutable data structure can be considered aliases, which need to be handled carefully.


In the following example, the `alter` function takes two mutable references to `Ref` structs, `a` and `b`, and modifies the `val` field of `a` to `10` and the `val` field of `b` to `20`. When we call `alter(x, x)`, we are essentially passing the same mutable reference, `x`, twice. As a result, the `val` field of `x` will be changed twice, as both `a` and `b` are just aliases referring to the same `x` reference.

```moonbit
fn alter(a: Ref[Int], b: Ref[Int]) -> Unit {
a.val = 10
Expand All @@ -72,13 +115,7 @@ fn init {
}
```

the `alter` function takes two mutable references to `Ref` structs, and then changes the value of the `val` field of both of them. This means that the `x` variable will also be changed twice, because it's just an alias for the `ref` variable.

## Debugger

Debugging is like being a detective. You're trying to figure out why your program isn't working as expected. MoonBit's debugger is a tool that helps you do this by showing you what's happening inside your program as it runs. You can pause the program at any point, look at the values of variables, and step through the code one line at a time. This is incredibly useful for understanding complex behavior and fixing bugs.

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

## Loops

Expand Down Expand Up @@ -113,9 +150,8 @@ fn init {
}
```

// Recursive form

```moonbit
// Recursive form
fn loop_(i: Int) -> Unit {
if i < 2 {
println("Hello!")
Expand All @@ -127,7 +163,7 @@ fn init {
}
```

### Controlling Loop Flow
### Controlling Loop Flows

Sometimes we want to control the flow of a loop more precisely. We might want to skip the rest of the current iteration or exit the loop entirely. In MoonBit, we can use `break` to exit a loop early or `continue` to skip the rest of the current iteration and move on to the next one.

Expand All @@ -149,14 +185,14 @@ fn print_first_3_break() -> Unit {
}
```

the excepted output is
The excepted output is

```text
1 yes
2 yes
```

but if we change `break` to `continue`
But if we change `break` to `continue`

```moonbit
fn print_first_3_continue() -> Unit {
Expand All @@ -174,7 +210,7 @@ fn print_first_3_continue() -> Unit {
}
```

the excepted output is
The excepted output is

```text
1 yes
Expand All @@ -188,17 +224,24 @@ the excepted output is
yes
```

- ## MoonBit Check
## Code Checking and Debugging

In order to avoid errors, we need to check our code. However, this work does not need to be done entirely by us, the MoonBit extension on VS Code can help us to some extent. Here are some examples:

MoonBit has some built-in checks to help us write better code. It checks if a variable that's supposed to be mutable has been modified, which can help us catch mistakes like forgetting to update a loop counter. It also checks that the return value of a function matches the declared return type, which helps us avoid type errors.
- If a mutable variable has not been modified, MoonBit extension will issue a warning, which can help us catch mistakes like forgetting to update a loop counter.
![](/pics/infinite_loop.webp)
- It also checks whether the return value of a function matches the declared return type, which helps us avoid typing errors.
![](/pics/forget_type.webp)

![MoonBit Check](/pics/moonbit_check.webp)
Sometimes, even our code passes the checking by our naked eyes and MoonBit extension, our code may still not behave as expected, which means there are bugs in our code. In that case, we need to debug our code. MoonBit's debugger is a tool that helps you debug your code by showing you what's happening inside your program as it runs. You can pause the program at any point, look at the values of variables, and step through the code one line at a time. This is incredibly useful for understanding complex behavior and fixing bugs.

## Mutable Data
![](/pics/debugger.webp)

Mutable data is data that can be changed after it's been created. This is different from immutable data, which can't be altered once it's defined. Mutable data can be very useful for things like directly controlling hardware, improving performance with data structures like arrays, constructing complex data structures, and saving space by modifying data in place instead of creating new copies.
## Trade-Offs of Mutable vs Immutable Data

While mutable data can make our programs more complex and harder to reason about, it doesn't necessarily conflict with the concept of referential transparency. For example, when calculating the Fibonacci sequence, even though we're using mutable data to store intermediate results, the final output is still determined solely by the input, so we can still reason about the function as if it were pure.
Although mutable data challenges the functional computational model we established before and may introduce some potential problems, it is still widely used in various scenarios. If we want to directly manipulate the external environment, e.g., hardware, it is better to use mutable data structures. When random access is needed, an mutable array typically performs better than a immutable list. Mutable data also makes it easier for us to construct complex data structures, such as graphs. In addition, in-place modification of mutable data can better utilize memory space, as it does not introduce additional space consumption.

Mutable data is not always in conflict with referential transparency. In the following example, we use a `while` loop and some mutable variables to calculate the `n`th term in the Fibonacci sequence. However, we can still safely replace any occurrence of `fib_mut` with its final result, since it does not produce any side effects.

```moonbit
fn fib_mut(n: Int) -> Int {
Expand All @@ -212,8 +255,6 @@ fn fib_mut(n: Int) -> Int {
}
```

But we can't say everying mutable will break referential transparency. For example, we can't say that the previous function is pure, because it modifies the value of `acc1`, `acc2` and `i`. But for the identical input, we can say that the output is always the same.

## Summary

In this chapter, we've explored the basics of imperative programming. We've learned about using commands to tell the computer what to do, variables to store values, and loops to repeat actions. Imperative programming is inherently different from functional programming, and it's important to understand the trade-offs between the two. By understanding these concepts, we can choose the right tools for the job and write programs that are both effective and easy to understand.

0 comments on commit a7c4ca8

Please sign in to comment.