Skip to content

Commit

Permalink
Improve doc of struct/enum/newtype/trait (#162)
Browse files Browse the repository at this point in the history
* improve doc of struct, enum and newtype

* slightly improve doc of traits

Co-authored-by: Oling Cat <[email protected]>
  • Loading branch information
Guest0x0 and OlingCat authored Mar 9, 2024
1 parent 53eaddf commit 5a18b99
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 62 deletions.
127 changes: 91 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ There are two ways to create new data types: `struct` and `enum`.

In MoonBit, structs are similar to tuples, but their fields are indexed by field names. A struct can be constructed using a struct literal, which is composed of a set of labeled values and delimited with curly brackets. The type of a struct literal can be automatically inferred if its fields exactly match the type definition. A field can be accessed using the dot syntax `s.f`. If a field is marked as mutable using the keyword `mut`, it can be assigned a new value.

```go live
```rust live
struct User {
id: Int
name: String
Expand All @@ -382,19 +382,9 @@ struct User {
fn init {
let u = { id: 0, name: "John Doe", email: "[email protected]" }
u.email = "[email protected]"
print(u.id)
print(u.name)
print(u.email)
}
```

Note that you can also include methods associated with your record type, for example:

```go
struct Stack {
mut elems: List[Int]
push: (Int) -> Unit
pop: () -> Int
println(u.id)
println(u.name)
println(u.email)
}
```

Expand Down Expand Up @@ -429,35 +419,90 @@ struct User {
id: Int
name: String
email: String
}

fn to_string(self : User) -> String {
"{ id: " + self.id.to_string() +
", name: " + self.name +
", email: " + self.email + " }"
}
} derive(Debug)

fn init {
let user = { id: 0, name: "John Doe", email: "[email protected]" }
let updated_user = { ..user, email: "[email protected]" }
println(user) // output: { id: 0, name: John Doe, email: [email protected] }
println(updated_user) // output: { id: 0, name: John Doe, email: [email protected] }
debug(user) // output: { id: 0, name: "John Doe", email: "[email protected]" }
debug(updated_user) // output: { id: 0, name: "John Doe", email: "[email protected]" }
}
```

### Enum

Enum types are similar to algebraic data types in functional languages. An enum can have a set of cases. Additionally, every case can specify associated values of different types, similar to a tuple. The label for every case must be capitalized, which is called a data constructor. An enum can be constructed by calling a data constructor with arguments of specified types. The construction of an enum must be annotated with a type. An enum can be destructed by pattern matching, and the associated values can be bound to variables that are specified in each pattern.
Enum types are similar to algebraic data types in functional languages. Users familiar with C/C++ may prefer calling it tagged union.

```go live
An enum can have a set of cases (constructors). Constructor names must start with capitalized letter. You can use these names to construct corresponding cases of an enum, or checking which branch an enum value belongs to in pattern matching:

```rust live
// An enum type that represents the ordering relation between two values,
// with three cases "Smaller", "Greater" and "Equal"
enum Relation {
Smaller
Greater
Equal
}

// compare the ordering relation between two integers
fn compare_int(x: Int, y: Int) -> Relation {
if x < y {
// when creating an enum, if the target type is known, you can write the constructor name directly
Smaller
} else if x > y {
// but when the target type is not known,
// you can always use `TypeName::Constructor` to create an enum unambiguously
Relation::Greater
} else {
Equal
}
}

// output a value of type `Relation`
fn print_relation(r: Relation) {
// use pattern matching to decide which case `r` belongs to
match r {
// during pattern matching, if the type is known, writing the name of constructor is sufficient
Smaller => println("smaller!")
// but you can use the `TypeName::Constructor` syntax for pattern matching as well
Relation::Greater => println("greater!")
Equal => println("equal!")
}
}

fn init {
print_relation(compare_int(0, 1)) // smaller!
print_relation(compare_int(1, 1)) // equal!
print_relation(compare_int(2, 1)) // greater!
}
```

Enum cases can also carry payload data. Here's an example of defining an integer list type using enum:

```rust live
enum List {
Nil
// constructor `Cons` carries additional payload: the first element of the list,
// and the remaining parts of the list
Cons (Int, List)
}

fn init {
// when creating values using `Cons`, the payload of by `Cons` must be provided
let l: List = Cons(1, Cons(2, Nil))
println(is_singleton(l))
print_list(l)
}

fn print_list(l: List) {
// when pattern-matching an enum with payload,
// in additional to deciding which case a value belongs to
// you can extract the payload data inside that case
match l {
Nil => print("nil")
// Here `x` and `xs` are defining new variables instead of referring to existing variables,
// if `l` is a `Cons`, then the payload of `Cons` (the first element and the rest of the list)
// will be bind to `x` and `xs
Cons(x, xs) => {
print(x)
print(",")
Expand All @@ -466,40 +511,49 @@ fn print_list(l: List) {
}
}

fn init {
let l: List = Cons(1, Cons(2, Nil))
print_list(l)
// In addition to binding payload to variables,
// you can also continue matching payload data inside constructors.
// Here's a function that decides if a list contains only one element
fn is_singleton(l: List) -> Bool {
match l {
// This branch only matches values of shape `Cons(_, Nil)`, i.e. lists of length 1
Cons(_, Nil) => true
// Use `_` to match everything else
_ => false
}
}
```

### Newtype

MoonBit supports a special kind of enum called newtype, which is used to create a new type from an existing type. A newtype can have only one case, and the case can have only one associated value.
MoonBit supports a special kind of enum called newtype:

```rust
// `UserId` is a fresh new type different from `Int`, and you can define new methods for `UserId`, etc.
// But at the same time, the internal representation of `UserId` is exactly the same as `Int`
type UserId Int
type UserName String
```

The name of the newtype is both a type and a data constructor. The construction and destruction of a newtype follow the same rules as those of an enum.
Newtypes are similar to enums with only one constructor (with the same name as the newtype itself). So, you can use the constructor to create values of newtype, or use pattern matching to extract the underlying representation of a newtype:

```rust
fn init {
let id: UserId = UserId(1)
let name: UserName = UserName("John Doe")
let UserId(uid) = id
let UserName(uname) = name
let UserId(uid) = id // the type of `uid` is `Int`
let UserName(uname) = name // the type of `uname` is `String`
println(uid)
println(uname)
}
```

Additionally, the value of a newtype can be accessed via the dot syntax.
Besides pattern matching, you can also use `.0` to extract the internal representation of newtypes:

```rust
fn init {
let id: UserId = UserId(1)
let uid = id.0
let uid: Int = id.0
println(uid)
}
```
Expand Down Expand Up @@ -758,7 +812,7 @@ fn init {

## Trait system

MoonBit features a structural trait system for overloading/ad-hoc polymorphism. Traits can be declared as follows:
MoonBit features a structural trait system for overloading/ad-hoc polymorphism. Traits declare a list of operations, which must be supplied when a type wants to implement the trait. Traits can be declared as follows:

```rust
trait I {
Expand All @@ -768,7 +822,8 @@ trait I {

In the body of a trait definition, a special type `Self` is used to refer to the type that implements the trait.

There is no need to implement a trait explicitly. Types with the required methods automatically implements a trait. For example, the following trait:
To implement a trait, a type must provide all the methods required by the trait.
However, there is no need to implement a trait explicitly. Types with the required methods automatically implements a trait. For example, the following trait:

```rust
trait Show {
Expand Down
Loading

0 comments on commit 5a18b99

Please sign in to comment.