From 5a18b99bc74b59fc625116f126eda2c9fcba0060 Mon Sep 17 00:00:00 2001 From: Guest0x0 <33187726+Guest0x0@users.noreply.github.com> Date: Sat, 9 Mar 2024 18:08:18 +0800 Subject: [PATCH] Improve doc of struct/enum/newtype/trait (#162) * improve doc of struct, enum and newtype * slightly improve doc of traits Co-authored-by: Oling Cat --- README.md | 127 ++++++++++++++++++++++++++++----------- zh-docs/README.md | 148 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 213 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 4f6c802f..628ee924 100644 --- a/README.md +++ b/README.md @@ -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 @@ -382,19 +382,9 @@ struct User { fn init { let u = { id: 0, name: "John Doe", email: "john@doe.com" } u.email = "john@doe.name" - 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) } ``` @@ -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: "john@doe.com" } let updated_user = { ..user, email: "john@doe.name" } - println(user) // output: { id: 0, name: John Doe, email: john@doe.com } - println(updated_user) // output: { id: 0, name: John Doe, email: john@doe.name } + debug(user) // output: { id: 0, name: "John Doe", email: "john@doe.com" } + debug(updated_user) // output: { id: 0, name: "John Doe", email: "john@doe.name" } } ``` ### 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(",") @@ -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) } ``` @@ -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 { @@ -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 { diff --git a/zh-docs/README.md b/zh-docs/README.md index 91d5bf5b..92316ec2 100644 --- a/zh-docs/README.md +++ b/zh-docs/README.md @@ -390,41 +390,118 @@ struct User { fn init { let u = { id: 0, name: "John Doe", email: "john@doe.com" } u.email = "john@doe.name" - print(u.id) - print(u.name) - print(u.email) + println(u.id) + println(u.name) + println(u.email) } ``` -注意,您还可以在结构类型中包含与之关联的方法,例如: +#### 创建结构体的简写形式 + +如果已经有和结构体的字段同名的变量,并且想使用这些变量作为结构体同名字段的值, +那么创建结构体时,可以只写字段名,不需要把同一个名字重复两次。例如: + +```rust live +fn init{ + let name = "john" + let email = "john@doe.com" + let u = { id: 0, name, email } // 等价于 { id: 0, name: name, email: email } +} +``` + +## 更新结构体的语法 + +如果想要基于现有的结构体来创建新的结构体,只需修改现有结构体的一部分字段,其他字段的值保持不变, +可以使用结构体更新语法: ```rust -struct Stack { - mut elems: List[Int] - push: (Int) -> Unit - pop: () -> Int +struct User { + id: Int + name: String + email: String +} derive(Debug) + +fn init { + let user = { id: 0, name: "John Doe", email: "john@doe.com" } + let updated_user = { ..user, email: "john@doe.name" } + debug(user) // 输出: { id: 0, name: "John Doe", email: "john@doe.com" } + debug(updated_user) // 输出: { id: 0, name: "John Doe", email: "john@doe.name" } } ``` + ### 枚举 -枚举类型对应于代数数据类型(Algebraic Data Type,ADT)中的和类型(sum type), +枚举类型对应于代数数据类型(Algebraic Data Type,ADT), 熟悉 C/C++ 的人可能更习惯叫它带标签的联合体(tagged union)。 -枚举可以有一组情况,并且每个情况和元组类似,可以指定不同类型的关联值。 -每个情况的标签必须大写,称为数据构造函数。 -枚举可通过调用数据构造函数,并传入对应类型的参数来构造。若上下文中的构造函数存在歧义时, -所赋予的变量必须标明类型,无歧义时则可省略类型标注。 -枚举可通过模式匹配来解构,并将关联值绑定到每个模式中指定的变量。 + +枚举由一组分支(构造器)组成,每个分支都有一个名字(必须以大写字母开头),可以用这个名字来构造对应分支的值, +或者在模式匹配中使用这个名字来判断某个枚举值属于哪个分支: + +```rust live +// 一个表示两个值之间的有序关系的枚举类型,有 “小于”、“大于”、“等于” 三个分支 +enum Relation { + Smaller + Greater + Equal +} + +// 计算两个整数之间的顺序关系 +fn compare_int(x: Int, y: Int) -> Relation { + if x < y { + // 创建枚举时,如果知道想要什么类型,可以直接写分支/构造器的名字来创建 + Smaller + } else if x > y { + // 但如果不知道类型,永远可以通过 `类型名字::构造器` 的语法来无歧义地创建枚举值 + Relation::Greater + } else { + Equal + } +} + +// 输出一个 `Relation` 类型的值 +fn print_relation(r: Relation) { + // 使用模式匹配判断 r 属于哪个分支 + match r { + // 模式匹配时,如果知道类型,可以直接使用构造器名字即可 + Smaller => println("smaller!") + // 但也可以用 `类型名字::构造器` 的语法进行模式匹配 + 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! +} +``` + +枚举的分支还可以携带额外的数据。下面是用枚举定义整数列表类型的一个例子: ```rust live enum List { Nil + // 构造器 `Cons` 携带了额外的数据:列表的第一个元素,和列表剩余的部分 Cons (Int, List) } +fn init { + // 使用 `Cons` 创建列表时,需要提供 `Cons` 要求的额外数据:第一个元素和剩余的列表 + let l: List = Cons(1, Cons(2, Nil)) + println(is_singleton(l)) + print_list(l) +} + fn print_list(l: List) { + // 使用模式匹配处理带额外数据的枚举时,除了判断值属于哪个分支, + // 还可以把对应分支携带的数据提取出来 match l { Nil => print("nil") + // 这里的 `x` 和 `xs` 不是现有变量,而是新的变量。 + // 如果 `l` 是一个 `Cons`,那么 `Cons` 中携带的额外数据(第一个元素和剩余部分) + // 会分别被绑定到 `x` 和 `xs` Cons(x, xs) => { print(x) print(",") @@ -433,41 +510,49 @@ fn print_list(l: List) { } } -fn init { - let l: List = Cons(1, Cons(2, Nil)) - print_list(l) +// 除了变量,还可以对构造器中携带的数据进行进一步的匹配。 +// 例如,下面的函数判断一个列表是否只有一个元素 +fn is_singleton(l: List) -> Bool { + match l { + // 这个分支只会匹配形如 `Cons(_, Nil)` 的值,也就是长度为 1 的列表 + Cons(_, Nil) => true + // 用 `_` 来匹配剩下的所有可能性 + _ => false + } } ``` ### 新类型 -MoonBit 支持一种特殊的枚举类型,称为新类型(newtype)。 -新类型只能从一个**现有**的类型创建(包括泛型),并且该新类型只能有**一种**枚举值的情况。 +MoonBit 支持一种特殊的枚举类型,称为新类型(newtype): ```rust +// `UserId` 是一个全新的类型,而且用户可以给 `UserId` 定义新的方法等 +// 但与此同时,`UserId` 的内部表示和 `Int` 是完全一致的 type UserId Int type UserName String ``` -新类型的名称既是类型又是数据构造函数。新类型的构造和解构遵循与枚举相同的规则。 +新类型和只有一个构造器(与类型同名)的枚举类型非常相似。 +因此,可以使用构造器来创建新类型的值、使用模式匹配来提取新类型的内部表示: ```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 // `uid` 的类型是 `Int` + let UserName(uname) = name // `uname` 的类型是 `String` println(uid) println(uname) } ``` -也可以使用 `.0` 对新类型进行解构: +除了模式匹配,还可以使用 `.0` 提取新类型的内部表示: ```rust fn init { let id: UserId = UserId(1) - let uid = id.0 + let uid: Int = id.0 println(uid) } ``` @@ -721,7 +806,8 @@ fn init { ## 接口系统 -MoonBit 具有用于重载/特设多态(ad-hoc polymorphism)的结构接口系统。接口可以声明如下: +MoonBit 具有用于重载/特设多态(ad-hoc polymorphism)的结构接口系统。 +接口描述了满足该接口的类型需要支持哪些操作。接口的声明方式如下: ```rust trait I { @@ -729,7 +815,17 @@ trait I { } ``` -接口无需显式实现,具有所需方法的类型会自动实现接口。例如,以下接口: +在接口声明中,`Self` 指代实现接口的那个类型。 + +一个类型要实现某个接口,就要满足该接口中所有的方法。例如,下面的接口描述了一个能够比较元素是否相等的类型需要满足的方法: + +```rust +trait Eq { + op_equal(Self, Self) -> Bool +} +``` + +接口无需显式实现,具有所需方法的类型会自动实现接口。考虑以下接口: ```rust trait Show {