2020年4月6日

rust08 Common Collections

Collection 可包含多個值。跟內建的 array 與 tuple 不同,collection 是儲存在 heap,資料的大小可隨時異動。以下討論三個最常用的 collection

  • vector: 可逐項儲存數量可變的值
  • string: 是 char 的集合,也就是先前用過的 String
  • hash map: key - value pair。這是 map 的一種特殊實作的版本。

std library 提供的其他 collection 的文件

Storing Lists of Values with Vectors

vector 可儲存多個值,在記憶體中是一個接著一個排列。只能儲存相同類別的值。例如,適合儲存文件的逐行文字資料,或是購物車裡的商品價格。

建立新的 Vector

注意要加上 <i32> 資料類別,因為 compiler 無法判斷沒有任何值的 Vector 的資料型別。

let v: Vec<i32> = Vec::new();

vec! 是 Macro,因為已經有初始的值,compiler 就能推測出 v 的類別是 Vec<i32>

let v = vec![1, 2, 3];

更新 Vector

要能改變 v,必須宣告為 mut 可變

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

Dropping a Vector Drops Its Elements

當 vector 離開 scope 被丟棄時,裡面的內容也會被丟棄。

{
    let v = vec![1, 2, 3, 4];

    // 使用 v

} // v 離開 scope 並被丟棄

讀取 Vector 的 elements

用 index 或是 get method,index 的結果是 reference,get method 回傳的結果是 Option<&T>

let v = vec![1, 2, 3, 4, 5];

// &v[2] 透過 index 取得第三個資料
let third: &i32 = &v[2];
println!("The third element is {}", third);

// 用 get method
match v.get(2) {
    Some(third) => println!("The third element is {}", third),
    None => println!("There is no third element."),
}

如果要取得超過長度的 index,就會在執行時發生 error,這部分錯誤無法在編譯時被發現。而 get 並不會 crash,而是回傳 None。

let v = vec![1, 2, 3, 4, 5];

// thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 100'
let does_not_exist = &v[100];

// None
let does_not_exist = v.get(100);

當程式獲得一個有效的 reference,borrow checker 會處理 ownership 及 borrowing rules 確保 vector 的引用永遠有效。ex: 因為在相同作用域中同時存在可變和不可變引用的規則,無法在獲得了 vector 的第一個元素的不可變引用後,嘗試在 vector 末尾增加一個元素。

let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6);

println!("The first element is: {}", first);

編譯時會發生錯誤

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable

在 vector 的結尾增加新元素時,可能發生沒有足夠空間將所有所有元素依次相鄰存放,這時候會分配新記憶體並將舊的元素複製到新的空間中。這時,第一個元素的引用就指向了被釋放的記憶體。借用規則會阻止程式陷入這種狀況。

Iterating over the Values in a Vector

用 for

let v = vec![100, 32, 57];
for i in &v {
    println!("{}", i);
}

// 也可以取得可變引用,然後修改裡面的值
let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

Using an Enum to Store Multiple Types

當需要在 vector 儲存不同類別的資料時,可以使用 enum。vector 裡面儲存的類別,就是相同的 enum

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

但如果一開始無法知道要儲存到 vector 的所有類別,就無法使用 enum,要改用 chap17 的 trait。

Storing UTF-8 Encoded Text with Strings

通常在 String 會遇到三個問題:rust 會確保找出所有可能的錯誤、String 資料結構比想像中複雜、UTF-8。

String 本身就是 collection of bytes,再外加一些 method 讓 bytes 可用 text 解譯。接下來會討論 String 跟其他 collections 的相異處,例如:因為人類跟機器解讀 String 方法不同,造成String 的 indexing 比較複雜。

What is a String?

rust 在核心中只有一種 string type: string slice str,通常會以引用的方式出現 &str。chap4 有討論過 string slices 就是 references 到某些存在別處的 UTF-8 encoded string data。例如 String literal,就是存在程式的 binary 中,因此也是 string slices。

rust std library 提供的 String type 是 growable, mutable, owned, UTF-8 encoded string type。當 Rustacean 談到 rust 的 "string" 時,通常是同時代表 String 以及 string slice &str 這兩個。雖然大部分都是關於 String,這兩個類別在 std 都被廣泛使用,同時他們都是 UTF-8 encoded。

std library 還有其他 string 類別,例如:OsString, OsStr, CString, CStr。還有其他 libray crate 提供更多 string。類別是以 String 或是 Str 結尾, 對應到 owned 及 borrowed variants。

Creating new String

    // 建立新的 String
    let mut s = String::new();

    let data = "initial contents";
    // to_string 可用在任何實現了 Display trait 的類別
    let s = data.to_string();

    // 用 string literal 的 to_string 產生 String
    let s = "initial contents".to_string();

    // 用 String::from 產生 String
    let s = String::from("initial contents");

    // 可以儲存 UTF-8 的字串
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שָׁלוֹם");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");

Updating a String

可使用 push_str, push 增加 string 內容

    // push_str 增加 string slice,但 push_str 不需要獲得該 string 的 ownership
    let mut s = String::from("foo");
    s.push_str("bar");

    // 如果 push_str 獲取了 s2 的 ownership,就會無法列印資料
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2 is {}", s2);

    // 用 push 增加 char
    let mut s = String::from("lo");
    s.push('l');

使用 +format! 連接字串

    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // 注意 s1 被移動了,不能繼續使用, s2 還可以繼續使用

    // + 的 函數定義類似這樣
    // fn add(self, s: &str) -> String {
    // &s2 是引用, add 只能將 String 及 &s 相加,不能將兩個 String 相加
    // 上面的 s2 會由 String 強制轉型 coerced 為 &str

    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    //let s = s1 + "-" + &s2 + "-" + &s3;
    // 可改用 format!,跟 println! 類似,且不會獲取 s2, s3 的 ownership
    let s = format!("{}-{}-{}", s1, s2, s3);

    println!("{}, {}, {}", s, s2, s3);

Indexing into Strings

如果用 index 語法取得 String 的一部分會發生錯誤,也就是說 rust 的 string 不支援 indexing

    let s1 = String::from("hello");
    let h = s1[0];

編譯錯誤

error[E0277]: the type `std::string::String` cannot be indexed by `{integer}`
 --> src/main.rs:5:13
  |
5 |     let h = s1[0];
  |             ^^^^^ `std::string::String` cannot be indexed by `{integer}`
  |
  = help: the trait `std::ops::Index<{integer}>` is not implemented for `std::string::String`

String 的實作方法

String 是一個 Vec<u8> 的封裝

    let len1 = String::from("Hola").len();
    // len 為 4

    let len2 = String::from("Здравствуйте").len();
    // len 為 24 不是 12,因為 unicode 每個字元需要 2 bytes

    println!("{}, {}", len1, len2);

因此 char 的 index 並不一定能對應到有效的 unicode。

假設 rust 可以這樣寫

let hello = "Здравствуйте";
let answer = &hello[0];

З 的第一個byte 為 208, 第二個是 151, answer 應該為 208,但 208 不是一個正常的 unicode。為了避免發生這樣的問題,rust 直接拒絕這樣的寫法,而是給我們編譯錯誤的訊息。

Bytes and Scalar Values and Grapheme Clusters

rust 有三種方式理解 string: bytes, scalar values, grapheme clusters (字形集合)

例如 印度語單詞 “नमस्ते” 存在 Vector 為

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

有 18 bytes,但從 unicode 來說,應該要是

['न', 'म', 'स', '्', 'त', 'े']

但第四與第六都不是字母,以 grapheme cluster 方式理解,應該為

["न", "म", "स्", "ते"]

另外因為 String 透過 index 取得 slice 的時間預期為 O(1),但 String 每次都必須要從開頭開始,無法確保O(1) 這樣的效能。

因此 rust 的 string 不支援 indexing

Slicing Strings

這是可能會造成程式 crash 的 method。可以用 [] 及 range 取得 string slice

    let hello = "Здравствуйте";

    // 這些字元都是 2 bytes
    // s 將會是 “Зд”
    let s = &hello[0..4];

    // 如果獲取 &hello[0..1] ,會造成程式 panic
    let s2 = &hello[0..1];
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2027:5

Iterating over Strings

for c in "नमस्ते".chars() {
    println!("{}", c);
}
न
म
स
्
त
े

也可以轉換為 bytes

for b in "नमस्ते".bytes() {
    println!("{}", b);
}

Storing Keys with Associated Values in Hash Maps

HashMap<K, V> 透過 hashing function 決定如何將 key, value 放入 memory

Creating a New Hash Map

    // HashMap 沒有倍 prelude 自動引用
    use std::collections::HashMap;

    // 類似 vector,HashMap 的 key 為 String, value 為 i32
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);


    // 也可以用 vector 的 collect 產生 Hash Map
    let teams  = vec![String::from("Blue"), String::from("Yellow")];
    let initial_scores = vec![10, 50];

    // 先透過 zip 產生 tuple 的 vector,再呼叫 collect
    let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
    println!("{:?}", scores);
    // {"Yellow": 50, "Blue": 10}

Hash Maps and Ownership

對於像 i32 實現 Copy trait 的類別,值可以複製到 Hash Map,但對於 String 有 ownership 的值,其值會因為 move 而轉職給 Hash Map

    use std::collections::HashMap;

    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // 這裡 field_name 和 field_value 不再有效,不能再使用

如果是用 reference,這些引用指向的值,雖 ownership 不會移動給 Hash Map,但必須在 map 有效時同樣有效。

存取 Hash Map 的 Values

    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    let team_name = String::from("Blue");
    // get 會回傳 Option<V>,如果不存在會回傳 None
    let score = scores.get(&team_name);
    println!("{:?}", score);
    // Some(10)


    // use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    // 用 for iterate Hash Map
    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
    //Yellow: 50
    //Blue: 10

更新 Hash Map

覆蓋舊的 value: insert

    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);

    println!("{:?}", scores);
    // {"Blue": 25}

只在沒有 value 時 insert: entry

    use std::collections::HashMap;

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    scores.entry(String::from("Yellow")).or_insert(50);
    // 不會改成 50
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{:?}", scores);
    // {"Yellow": 50, "Blue": 10}

根據舊的值,更新新值

例如記錄某個單詞出現幾次

    use std::collections::HashMap;

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

    // or_insert 會回傳 &mut V,可變引用
    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{:?}", map);
    // {"wonderful": 1, "hello": 1, "world": 2}

Hashing Functions

HashMap 預設使用 "cryptographically strong" hashing function,可防止 Denial of Service (DoS) 攻擊。這不是最快的演算法,但犧牲性能提高安全性。可利用 hasher 切換使用其他 hashing function。hasher 是實作 BuildHasher trait 的類別。crates.io 可找到其他常用的 hasher hashing function。

References

The Rust Programming Language

中文版

中文版 2

2020年3月30日

rust07 Packages, Crates, and Modules

程式都會遇到 scope 的問題,在某一個時間點,compiler 知道些變數,允許呼叫哪些函數,變數引用是什麼?

rust 有 module system

  • packages: 是 cargo 的功能,可建立、測試、分享 crate
  • crates 是一個 module 的樹狀結構,有 library 或 binary project
  • modules 和 use: 允許你控制 scope 及 privacy of paths
  • paths: 對 struct, function, module 命名的方法

Packages and Crate

  • crate 是 binary 或是 library
  • crate root 一份描述如何建構 create 的文件
  • 一個 Package 是一個或多個 crates
  • Package 透過 Cargo.toml 用來描述建構 crates

  • package 可包含 0 或 1 個 library crate,不能超過。可包含多個 binary crates。至少要包含一個 crate (library 或 binary 都可以)

cargo new 就是在產生一個新的 package

$ cargo new my-project
     Created binary (application) `my-project` package

cargo package 預設就是在 Cargo.toml 同一層的目錄中,有包含 src 目錄,且裡面有 main.rs 時, cargo 就知道這個 package 帶有一個跟 package 同名的 binary crate,且 src/main.rs 就是 crate root。另外如果 src/lib.rs,其 package 帶有同名的 library crate,且 src/lib.rs 是 crate root。 crate root 檔案將會傳給 rust 用來建構 library 或是 binary。

package 可包含 0 或 1 個 library crate,不能超過。可包含多個 binary crates。至少要包含一個 crate (library 或 binary 都可以)

如果 package 同時包含 src/main.rs 及 src/lib.rs,就是帶有兩個 crate: 一個同名 library,一個同名 binary。package 可帶有多個 binary crate,要將其檔案放在 src/bin 目錄,每個檔案都是一個獨立的 binary crate。

Defining Modules to Control Scope and Privacy

module system

  • module: 組織 code 與控制路徑私有性的方法
  • paths: 命名 items 的方法
  • use: 將某個 path 帶入 scope
  • pub: 開放 items,讓 items 變成公有
  • as: 將 item 引入 scope 時,用來 rename
  • external packages
  • glob operator: 將 module 的所有內容引入 scope
  • 將不同 module 分割到獨立的檔案中

module 可組織程式碼,將 crate 分組,增加 readability 與 reuse 便利性。module 也可以控制 privacy of items,item 可以是 public (可被 outside code 使用),或是 private,只是 internal implementation。

以下是 restaurant 的 library crate 範例

在一個餐廳可分兩個部分:front of house 及 back of house。front of house 包含 seating customers, 收單的 servers,payment,製作飲料的 bartenders。back of house 包含chef,在廚房燒菜,洗碗機,及負責管理的 managers。

首先建立一個新的 library

cargo new --lib restaurant

修改 src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

modules 可包含其他 items 的定義:structs, enums, constants, traits, functions

module 可將相關的定義放在一起。

src/main.rs, src/lib.rs 分別稱為 crate roots

以上就是產生了這樣的 module tree。類似 file system 的目錄樹,另一個類似的地方,是需要用 path 來找到適當的 module 或 item。

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

Paths for Referring to an item in the Module Tree

rust 以 path 在module tree 中定位 item,如果要呼叫某個 function 就必須要知道他的 path

path 有兩種形式,兩種都有一或多個 identifiers,id 之間以 :: 區隔

  • absolute path: 從 crate root 開始,以 crate 開頭
  • relative path: 從目前的 module 開始,以 self, super 或目前的 module 的 id 開始
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

在編譯時,會發生 error ,這是 privacy 的問題

error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^

rust 的所有 items: funcitons, methods, structs, enums, modules, constants 預設都是 private。在 parent module 的 items 不能使用 child modules 裡面的 private items。但 child modules 的 items 可以使用 ancestor modules 的 items。原因是 child modules 會 wrap & hide implementation details,但 child modules 可看到跟其定義相同的 context 的其他 items。

rust 會隱藏 inner implementation details,因此我們可知道如何在不影響 outer code 的狀況下,改變 inner code。可利用 pub keyword 讓 item 變成 public。

Exposing Paths with pub

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// 因為 eat_at_restaurant 跟 front_of_house 是相同的 module,故可直接使用 front_of_house
pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Starting Relative Paths with super

super 類似 filesystem 的 .. 用途,代表 parent module。

backofhouse 跟 serve_order 在相同的 module

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();

        // super 就等同於 crate
        super::serve_order();
    }

    fn cook_order() {}
}

Making Structs and Enums Public

如果在 struct 定義前面加上 pub,可讓 struct 變成 public,但 struct 裡面的欄位還是 private。必須單獨每一個欄位分開設定 pub

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    // 為 Breakfast 增加 public method: summer
    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast 訂早餐
    let mut meal = back_of_house::Breakfast::summer("Rye");

    // Change our mind about what bread we'd like 修改餐點
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // 無法修改 seasonal_fruit,因為該欄位是 private
    // meal.seasonal_fruit = String::from("blueberries");
}

對於將 enum 設定為 public,只需要在定義的地方加上 pub,就會讓所有 variants 都是 public

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Bringing Paths info Scope with use

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// 原本在使用 hosting,必須要透過 相對或絕對路徑
// 透過 use 宣告後,後面就可直接使用 hosting
use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

use 要使用相對路徑有一點點不同,路徑必須要用 self 開頭。不過目前實測,如果不寫 self 也沒有問題。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// self 也可以不寫
//use self::front_of_house::hosting;
use front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Creating Idiomatic use Paths

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// use 可以將 add_to_waitlist 引入,這樣後面的程式,就可以直接呼叫這個 fn
// 但這樣是不好的寫法
// 因為使用 add_to_waitlist 時,感覺 add_to_waitlist 是定義在這個 module 裡面
// 而上面 hosting::add_to_waitlist(); 這樣的呼叫方式
// 比較清楚知道,add_to_waitlist 是定義在其他地方
use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
    add_to_waitlist();
    add_to_waitlist();
}

當 use structs, enums, 其他 items 時,最好指定 full path。例如指定使用標準函式庫的 HashMap

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

唯一的例外是,在 scope 中,使用了兩個相同名稱的 items,這時必須要加上 parent module,用來區分 items

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
}

fn function2() -> io::Result<()> {
}

Providing New Names with as

使用相同名稱的 items 有另一種解決方法,就是用 as 加上替代的 new local name

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
}

fn function2() -> IoResult<()> {
}

Re-exporting Names with pub use

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// 在 use 前面再加上 pub
// 就等於將 hosting 變成 public,外面的 code 也可以直接使用 hosting::add_to_waitlist()
pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Re-exporting 在程式碼的 internal structure 跟外面呼叫你的 code 的方法不同時,會很有用。例如餐廳,營運者很容易了解 "front of house" "back of house" 的意義。但客戶可能不懂這兩個名詞,這時候,可以透過 pub use 調整結構。

Using External Packages

先前專案有使用 rand 這個 external package,是在 Cargo.toml 裡面加上 rand

[dependencies]
rand = "0.5.5"

加上 rand 的定義後,等於將 rand 帶入這個 package 的 scope。透過 use 引用 Rng

use rand::Rng;
fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 101);
}

Rust community 將許多 library packages 都放在 https://crates.io

標準函式庫 std 也是一個 crate。但因為 std 是在 rust language 裡面,所以不需要另外在 Cargo.toml 裡面填寫到 dependencies 中。

Using Nested Paths to Clean Up Large use Lists

如果同時要使用兩個以上的 module,但 std:: 會一直重複。

use std::cmp::Ordering;
use std::io;

改用這個方式

use std::{cmp::Ordering, io};

另一個例子

use std::io;
use std::io::Write;

// 改為
use std::io::{self, Write};

The Glob Operator

如果想使用某個 path 的所有 public items,就用 *

use std::collections::*;

Separating Modules info Different Files

先前的範例都是將多個 module 定義在一個檔案中。可以將 module 分在不同檔案。

修改 src/lib.rs

// 定義在 src/front_of_house.rs
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

src/frontofhouse.rs

pub mod hosting {
    pub fn add_to_waitlist() {}
}

可再進一步分離 hosting

修改 src/frontofhouse.rs

pub mod hosting;

新增 src/frontofhouse/hosting.rs

pub fn add_to_waitlist() {}

References

The Rust Programming Language

中文版

中文版 2

2020年3月23日

rust06 enums and pattern matching

enumerations 也稱為 enums,可列舉所有可能的值,定義一個新的類別。首先會了解如何定義 enums,以及資料編碼的方法。然後會了解一個特殊的 enum,就是 Option,代表不是某個值,就是另一個值。然後介紹 pattern match expression,最後是 if let,一個方便處理 enum 的方法。

rust 的 enum 類似 F#, OCaml, Haskell 的 algebraic data types

Defining an enum

假設要處理 IPv4, IPv6 兩種 IP Address,程式會遇到兩種 IP,故要用 enum 列出所有可能的值。

任何一個 IP,不是 IPv4 就是 IPv6,不能兩個都是,但 IPv4, IPv6 都是 IP Address。

enum IpAddrKind {
    V4,
    V6,
}

enum values

產生兩個不同成員的 instances,IpAddrKind::V4IpAddrKind::V6 都是 IpAddrKind 類別。

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;   

可定義使用 IpAddrKind 的 function,並用 IpAddrKind::V4IpAddrKind::V6 呼叫該 function

fn route(ip_type: IpAddrKind) { }

route(IpAddrKind::V4);
route(IpAddrKind::V6);

另外用 struct 儲存實際的 IP Address

enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

可以用另一種更簡潔的方式,表達相同的概念,就是直接將資料放入 enum,這樣就不需要 struct。

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

用 enum 代替 struct 還有其他優點:每個成員可以處理不同類與數量的資料。

例如將 IPv4 分成四個 0~255 的數字

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

標準函式庫提供了 IPv4, IPv6 的定義:

struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

可以將任意資料放入 enum 成員中,例如 String, 數字, structs,放入另一個 enum 也可以。

另一個範例,裡面有不同類別的值

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

這跟用多個不同的 struct 定義很類似,但 struct 會是不同的類別,不能像 enum message 一樣,可定義一個同時能處理這些類別的 function

struct QuitMessage; // 類單元結構體
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元組結構體
struct ChangeColorMessage(i32, i32, i32); // 元組結構體

enum 也可以用 impl 定義 method,這個範例是定義 call 這個 method

impl Message {
    fn call(&self) {
        // 在這裡定義方法body
    }
}

let m = Message::Write(String::from("hello"));
m.call();

The Option Enum and Its Advantages Over Null Values

Option 是標準函式庫定義的另一個 enum。代表有值或是沒有值。這也就是說,可以處理掉 Null 造成的問題。

rust 並沒有其他語言都有的 null value,在有 null 的程式語言中,變數通常是 null 或是 non-null

null 的發明者 Tony Hoare 在 2009年"Null References: The Billion Dollar Mistake" 演講裡面說,null 是一個 billion-dollar mistake,原本他是要設計一個可透過 compiler 自動檢查,保證所有引用的使用都是安全的。但他另外增加了 null reference,因為他很容易實作出來。但這是一個很容易發生的 error, vaulerabilities, system crash,也就是這四十多年造成了數十億美元的損失。

使用 null value 會造成 error,非常容易出現。

但 null 本身還是有意義的,代表目前無效或是不存在的值。

rust 沒有 null,但有提供 Option<T>,包含在 prelude 中,也不需要 Option:: 就能直接使用 SomeNone

enum Option<T> {
    Some(T),
    None,
}

宣告時,如果使用 None,必須明確定義類別,因為 compiler 不知道怎麼判斷類別。

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

Option<T> 比 null 好的原因是資料類別檢查,另外不需要擔心變數是不是空值,只要不是 Option<T> 就不可能是空值。

例如無法將 Option<i8>i8 相加

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

可閱讀 Enum std::option::Option 文件,了解要如何使用 Option

例如用 match 處理 Some 與 None 兩種狀況

    match self {
        Option::Some(val) => val,
        Option::None =>
            panic!("called `Option::unwrap()` on a `None` value"),
    }

The match Control Flow Operator

match 可將 value 與一系列模式相比較匹配,模式可由 literal, 變數, wildcards 或其他內容組成。

match 可想像為硬幣分類器。valueincents 可取得硬幣的價值。

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

match 很像 if,但 if expression 要回傳 bool,而 match 可回傳任意類別。

match 會依照順序進行比較,會執行匹配的第一個值,回傳該值後面的 expression 回傳值。也可以用 {} 加上多行程式碼。

Patterns that Bind to Values

match 的另一個功能是綁定匹配模式的部分值,也就是從 enum 成員中提取值。

ex: 1999~2008,US 在每個州印刷了不同的 25 美分,但其他硬幣就沒有這種設計,將 State 這個資訊加入 enum 改變 Quarter。

Quarter 成員存放了一個 UsState 值的 Coin enum,匹配到 Coin::Quarter 時,state 將會綁定 25 美分硬幣所對應州的值。接著在那個分支的代碼中使用 state

#[derive(Debug)] // 這樣可以可以立刻看到州的名稱
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        },
    }
}

呼叫 value_in_cents(Coin::Quarter(UsState::Alaska))時,coin 將是 Coin::Quarter(UsState::Alaska)。當將值與每個分支相比較時,直到遇到 Coin::Quarter(state)。這時,state 綁定的將會是值 UsState::Alaska。接著就可以在 println! 表達式中使用這個綁定了,像這樣就可以獲取 Coin 枚舉的 Quarter 成員中內部的州的值。

Matching with Option<T>

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

Matches Are Exhaustive

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

剛剛的 plus_one 如果忘了匹配 None,就會發生編譯錯誤

error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:2:11
  |
2 |     match x {
  |           ^ pattern `None` not covered

rust 的 matching 是 exhaustive 的,必須要窮舉,將所有可能性都列出來。

The _ Placeholder

如果真的不想列出所有可匹配的條件,可以用 _替代。

最後的 _ 會匹配所有上面沒有的值

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

但這樣在只關心一種條件的狀況,語法就太多。因此 rust 提供了 if let

Concise Control Flow with if let

let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

可以用 if let 替代,程式碼會比較少,但會失去 match 的 exhaustive 窮舉特性

if let Some(3) = some_u8_value {
    println!("three");
}

可以在 if let 再加上 else

let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}

上面的程式碼可以改寫為

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

References

The Rust Programming Language

中文版

中文版 2

2020年3月16日

rust05 Structs

struct 是自訂資料類別,可包裝多個相關的 values。如果是物件導向程式語言, struct 就像是物件中的 attributes,以下會討論 tuple 跟 struct 的差異,說明 struct 的使用方法,並討論如何定義 method 與相關函數,來限制 structs 資料的行為。Structs 與 Enum 都是建立新類別的方法,並充分運用了 Rust 的編譯時期資料型別檢查的功能。

Defining and Instantiating Structs

struct 跟 tuple 類似,每一個部分可以是不同的資料型別,但不同於 tuple,struct 需要為每一個部分的資料命名,表達其值的意義。因為有了名字,struct 比 tuple 靈活,不需要靠順序來存取裡面的值。

struct 的每一個部分的資料稱為 field

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

定義 struct 後,透過 key:value 名稱來存取裡面的值,例如下面是修改 user1 的 email

let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");

Using the Filed Init Shorthand when Variables and Fields Have the same value

這是產生 user 的 function

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

當 field 的名稱與 value 都相同時,email 以及 username 都是一樣的,可使用 field init shorthand 改寫這個 function

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

Creating Instances From Other Instances With Struct Update Syntax

透過 struct update syntax 這種語法更新 struct 裡面的 field

這是用 user1 的兩個欄位,產生新的 user2

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

可改用這種語法

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

Using Tuple Structs without Named Fields to Create Different Types

tuple 跟 struct 類似,但沒有欄位名稱。如果想要為 tuple 命名,可使用 tuple structs

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

雖然都是一樣的結構,但 black 跟 origin 是不同的類別

Unit-Like Structs Without Any Fields

unit-line struct 類似於 (),就是 unit 類別,裡面沒有任何 fields。可用在想讓某個類別實現 trait,但不需要再類別中儲存資料的狀況。

Ownership of Struct Data

如果在 Struct 中使用了 String,而不是 &str 的 slice,這就是要讓這個 struct 自己擁有這個欄位的資料,只要 struct 有效,資料就有效。

也可以讓 struct 儲存被其他類別擁有的資料的 reference,但就需要用到 lifetimes 的概念。

lifetime 可確保 struct 引用的資料有效性跟 struct 本身保持一致。

struct User {
    username: &str,
    email: &str,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

編譯錯誤

error[E0106]: missing lifetime specifier
 --> src/main.rs:2:15
  |
2 |     username: &str,
  |               ^ expected lifetime parameter

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:12
  |
3 |     email: &str,
  |            ^ expected lifetime parameter

chap 10 會討論 lifetime,解決這邊的錯誤。

一個使用 struct 的範例

撰寫一個計算長方形面積的程式。

先用基本的變數方式撰寫

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

fn area 有兩個參數,但沒有表現出相關的特性,可以改用 tuple 或 struct

這是 tuple 的版本,但沒有明確說明哪一個參數是長,哪一個是寬

fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

用 struct 改寫

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

利用 derived traits 增加功能

以下程式,在列印 Rectangle 時會發生編譯 error

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {}", rect1);
}

編譯錯誤

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
 --> src/main.rs:9:29
  |
9 |     println!("rect1 is {}", rect1);
  |                             ^^^^^ `Rectangle` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Display::fmt`

println! 巨集裡面的 {},預設是使用 Display 格式,對於 struct 並沒有 Display。錯誤訊息說明,要加上 #[derive(Debug)],並改用 {:?}

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {:?}", rect1);
}

#[derive(Debug)]是利用註解來 derive Debug trait,而 {:?} 是列印的格式,結果為

rect1 is Rectangle { width: 30, height: 50 }    

如果改用 {:#?} ,輸出資料就會變成

rect1 is Rectangle {
    width: 30,
    height: 50
}

rust 提供了很多透過 dervie 註解使用的 trait,可謂自訂類別增加一些常用的功能。

Method Syntax

method 跟函數一樣,都適用 fn 宣告,可以有參數及回傳值,但 method 跟 function 不同,在 struct (或是 enum, trait) 的 context 中被定義,第一個參數永遠是 self,就是呼叫該 method 的 struct instance。

Defining Methods

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // self 是 Rectangle,必須加上 &
    // method 可以取得 self 的 ownership,也可以用 &self 不可變借用,或是 &mut self 可變借用
    // 如果想要在 method 裡面改變資料,就要改用 &mut self
    // 很少會直接寫成 self
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

where's the -> operator ?

C/C++ 有兩種方式呼叫 method, . 直接在 object 上呼叫 method 或是 -> 在 object 的 pointer 呼叫 method。這邊要先了解 dereference pointer。假設 object 是 pointer,那麼 object->something() 就跟 (*object).somethong() 一樣。

object.something() 呼叫 method 時,rust 會自動為 object 加上 &, &mut* ,以便讓 object 跟 method signature 匹配。

這兩種寫法是一樣的

p1.distance(&p2);
(&p1).distance(&p2);

自動引用是因為 method 有明確的接收者 self,在提供了接收者與 method 名稱條件下,rust 可計算出要使用 &self, &mut self 或是 self

Methods with More Parameters

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // 取得另一個矩形的 immutable reference,只讀取資料,不需要寫入
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Associated Functions

impl 區塊的另一個功能:允許在 impl 中定義,不使用 self 為參數的函數,這種函數稱為 assoicated function。因他們與 struct 結構相關,是函數而不是 method,因為他們不作用於一個 struct 的 instance。

產生 struct 新 instance 的 factory function,常常是用 associated function 實作。

接受一個參數,產生正方形 Rectangle

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

:: 呼叫 associated function,例如 let sq = Rectangle::square(3);

Multiple impl blocks

每個 struct 都允許有多個 impl blocks

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

References

The Rust Programming Language

中文版

中文版 2

2020年3月9日

rust04 Ownership

ownership 是 rust 中特別的功能,因為 rust 沒有 garbage collection 的功能,因此要注意 ownership 的運作方式:borrowing, slices, 並瞭解 rust 如何在記憶體中存放資料。

What is Ownership?

所有的程式都必須要管理如何使用機器的記憶體。第一種使用 GC 機制,裡面內建一個自動處理的排程,不斷地自動回收不被使用的記憶體,第二種是交給 Programmer 自己配置與釋放記憶體。Rust 選擇第三種,利用 a system of ownership 所有權系統管理記憶體。編譯器在編譯時,會進行規則的檢查,而在執行時,所有權系統完全不會拖慢程式的速度(swift 也是使用這個方式,實作了 Automatic Reference Counting ARC)。

Stack and Heap

在很多程式語言中,不需要考慮資料是存在 Heap 或是 Stack,但 Rust 要注意 Stack 或是 Heap 會影響到程式語言的行為。

Stack 是 Last In First Out 的順序,有 Push 資料,Pop 資料的動作。

Stack 的 IO 很快,資料存取的位置一直在 Stack 的頂端,不需要尋找存放或讀取資料的位置。另外 Stack 裡面的資料都必須要佔用已知且固定的大小。

在編譯程式時,大小未知或是可能會改變的資料,要儲存在 Heap,Heap 是混亂的,當儲存資料前,必須要要求一塊固定大小的空間。OS 在 heap 找到一塊夠大的空間,標記為已使用,並回傳 pointer。這個動作稱為 allocating on the heap。因為 pointer 大小已知且固定,可以將 pointer 存放在 stack,但要取得實際資料的內容時,還是要透過 pointer 取得資料。

因 heap 要透過 pointer 存取,會比 stack 慢。OS 在處理比較近(stack)的兩份資料時,會比處理比較遠的 heap 較快。

在程式呼叫一個函數時,傳給函數的值(可能是指到 heap 的 pointer)以及函數的 local 變數,都會被 push 到 stack,當函數結束就會被移出 stack。

Ownership 系統要處理的事:追蹤那個 code 正在使用 heap 的哪些資料,最大限度減少 heap 上重複的資料數量,清理不在被使用的資料,確保不會耗用記憶體空間。因此 ownership 系統就是用來管理 heap 的資料。

Ownership Rules

  1. Each value in Rust has a variable that’s called its owner. 每一個值的背後都有一個稱為 owner 的變數
  2. There can only be one owner at a time. 每一個值都只有一個 owner
  3. When the owner goes out of scope, the value will be dropped. 當 owner(也就是變數) 離開作用域 scope 後,這個值就會被丟棄。

Variable Scope

{                      // s 在這裡無效, 它尚未宣告
    let s = "hello";   // 從此處起,s 是有效的

    // 可以使用 s
}                      // 此作用域已結束,s 不再有效

String 類別

一般的字串 literal 會被直接 hard coded 到程式裡面,雖然方便,但因為是不可變的,不適合在某些狀況使用,有時一開始會不知道字串的值,例如從 stdin 取得輸入的字串。 Rust 有第二種字串類別 String,這種類別會被配置到 heap,可儲存在編譯時期未知的字串。

let s = String::from("hello");

s.push_str(", world!"); // push_str() 增加後面的字串

println!("{}", s); // 列印

String 可變但是 "string literal" 卻不可改變的原因,在於兩種類別處理記憶體的方式不同。

Memory and Allocation

string literal 在編譯時就已經知道內容,故內容會直接編碼到最後的執行檔中,這種方式可快速存取。String 用來處理可變長度的字串,因為可變,就需要在 heap 配置一塊編譯時未知大小的記憶體來存放內容。

  • 要在執行時,向 OS 取得 memory
  • 當使用完 String 後,要有將記憶體退還給 OS 的方法

第一步是當我們呼叫 String::from 時,就會取得 memory。但第二步,在有 GC 的語言中,GC 會自動處理。如果沒有 GC,就要在程式中,自己釋放記憶體,就是 allocate 後的 free,過早或是忘記 free 都會造成 bug。

Rust 是第三種機制,當變數離開 scope 後,就會被自動釋放。

{
    let s = String::from("hello"); // 從此處起,s 是有效的

    // 使用 s
}                                  // 此作用域已結束,
                                   // s 不再有效

當 s 離開 scope,Rust 會呼叫一個特殊的函數 drop,也就是在 } 之前,呼叫 drop

C++ 這種 item 在生命週期結束時,釋放資源的模式稱為 Resource Acquisition Is Initialization (RAII)

當有多個變數同時在 heap 使用 memory 時,就會有一些特殊的狀況

變數跟資料的互動方式 (一):move

let x = 5;
let y = x;

x, y 都是 5,所以有兩個 5 放入 stack 中

let s1 = String::from("hello");
let s2 = s1;

雖然程式碼跟上面類似,但實際上並不是複製一份。

首先 s1 的 pointer 會指向 heap 的記憶體位置,另外記錄長度及容量。

當 s1 賦值給 s2,並沒有複製 heap 的資料,而是複製了 s1 的 pointer, len, capacity 到 s2

如果用類似 string literal 的做法,複製了 heap 的資料,會造成記憶體的消耗,效能低落。

如果 s1, s2 離開 scope,這時候要釋放記憶體,會發生 s1, s2 做了兩次釋放 double free 的動作,兩次釋放會造成記憶體損壞,也就是安全漏洞。

為確保記憶體安全,在以下這種狀況,當 s1 指派給 s2 後,就不能再使用 s1 了,因為 rust 認為 s1 已經無效。以下這樣的程式碼,無法被執行。

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);
warning: unused variable: `s2`
 --> src/main.rs:3:9
  |
3 |     let s2 = s1;
  |         ^^ help: consider prefixing with an underscore: `_s2`
  |
  = note: #[warn(unused_variables)] on by default

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

這種做法相對於 deep copy,比較像是 shallow copy,但因為 rust 讓第一個變數無效了,也就是用了 move 的動作,這不同於 shallow copy。因為只有 s2 有效,因此離開 scope,就只需要處理 s2 的釋放。

變數跟資料的互動方式 (二):clone

如果確實需要 deep copy 某個 String 在 heap 上的資料,可以使用 clone

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

記憶體的狀況會是這樣

Stack-Only Data: Copy

這邊沒有呼叫 clone,但 x 還是可以使用。原因是整數這種已知大小的資料,是儲存在 stack,而 stack 的資料是直接 copy。

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

rust 有一種稱為 Copy trait 的特殊註解,可用在這邊。如果某一個類別有 Copy trait,舊的變數賦值給其他變數後,還可以持續使用。

rust 不允許實作了 Drop trait 的類別使用 Copy trait。如果對其值離開 scope 需要做特殊處理的類別,使用 Copy 註解,就會發生編譯錯誤。

可查看該類別的文件,檢查是不是有 Copy trait。但有個規則,任何簡單的 scalar 的組合,都是可以 Copy,不需要配置記憶體或是某個資源的類別,是可以 Copy

  • 所有整數類別 ex: u32
  • bool 也就是 true, false
  • 所有浮點數類別 ex: f64
  • 字元 chat
  • tuple,且裡面的元素都是可以 Copy 的時候 ex: (i32, i32)

Ownership and Functions

將值傳給函數,在語義上跟給變數賦值類似。向函數傳遞值,可能會被移動 move 或複製 copy

fn main() {
    let s = String::from("hello");  // s 進入作用域

    takes_ownership(s);             // s 的值移動到函數裡 ...
    // ... 所以到這裡 s 不再有效

    let x = 5;                      // x 進入作用域

    makes_copy(x);                  // x 應該移動函數裡,
    // 但 i32 是 Copy 的,所以在後面可繼續使用 x

} // 這裡, x 先移出了作用域,然後是 s。但因為 s 的值已被 move,所以不會有特殊操作

fn takes_ownership(some_string: String) { // some_string 進入作用域
    println!("{}", some_string);
} // 這裡,some_string 離開作用域並呼叫 `drop` 方法。佔用的內存被釋放

fn makes_copy(some_integer: i32) { // some_integer 進入作用域
    println!("{}", some_integer);
} // 這裡,some_integer 離開作用域。不會有特殊操作

Return Values and Scope

return value 也可以轉移 ownership

fn main() {
    let s1 = gives_ownership();         // gives_ownership 將回傳值
    // 移給 s1

    let s2 = String::from("hello");     // s2 進入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移動到
    // takes_and_gives_back 中, 
    // 它也將回傳值移給 s3
} // 這裡, s3 離開作用域並被丟棄。s2 也離開作用域,但已被 move,所以什麼也不會發生。s1 移出作用域並被丟棄

fn gives_ownership() -> String {             // gives_ownership 將返回值移動給
    // 調用它的函數

    let some_string = String::from("hello"); // some_string 進入作用域.

    some_string                              // 回傳 some_string 並移出給調用的函數
}

// takes_and_gives_back 將傳入字符串並回傳該值
fn takes_and_gives_back(a_string: String) -> String { // a_string 進入作用域

    a_string  // 回傳 a_string 並移出給調用的函數
}

變數的 ownership 遵循相同的模式:賦值給另一個變數,就會 move。當持有 heap 資料的變數離開 scope,就會被 drop,除非該資料被移動給另一個變數,轉移了 ownership

如果想要將某個變數傳給一個函數,但不讓該函數獲取 ownership,簡單的做法就是將該變數在函數回傳的時候,利用 tuple 一併傳回來。

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();

    (s, length)
}

不過這種做法有點麻煩,rust 提供另一個機制:引用 references

References 引用 and Borrowing 借用

傳給函數的是 &s1,函數定義中是 &String, & 就是引用,可允許使用值,但不獲取其 ownership

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {  // s 是對 String 的引用
    s.len()
} // 這裡,s 離開了作用域。但因為它並不擁有引用值的所有權,所以不會發生什麼問題

這裡將獲取 Reference 作為函數參數的動作稱為 Borrowing 借用。借用得來的值,無法被修改,會發生編譯錯誤。

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut std::string::String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

Note: 就 C 語言來說 &s 代表該記憶體的位址的值,呼叫函數時,只是將記憶體的位址,傳入函數,函數內再透過 pointer 取得字串的 pointer, len , capacity

Mutable References

上一個程式的問題,可以用 mutable reference 解決

&s 改為&mut s

&String 改為 &mut String

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

但 mutale reference 有個限制:特定 scope 中的特定資料,有且只能有一個 mutable reference。

這是錯誤的程式碼

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

編譯錯誤

error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

只要離開 scope,就可以建立一個新的 mutable reference

let mut s = String::from("hello");

{
    let r1 = &mut s;

} // r1 在這裡離開了作用域,所以我們完全可以創建一個新的引用

let r2 = &mut s;

這種限制,可在編譯其避免 data race,data race 由以下行為發生

  • 兩個或更多 pointer 同時存取同一個資料
  • 至少有一個 pointer 用來寫入資料
  • 沒有同步存取資料的機制

data race 會造成問題,且在執行期很難追蹤跟修復,rust 直接禁止這種行為發生。

在不可變跟可變引用混用時,也會發生同樣的問題

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

Dangling Reference

在有支援 pointer 的程式語言中,會因為釋放 memory 後,由原本保留的 pointer 取得錯誤的資料,也就是 dangling pointer,rust compiler 可確保 reference 永遠不會發生 dangling reference 的狀況。

fn main() {
    let reference_to_nothing = dangle();
}

// 回傳一個字串的 reference
fn dangle() -> &String {
    let s = String::from("hello");

    &s
} // s 在離開 scope 時,會被丟棄,記憶體被釋放,但回傳了 s 的 reference,會造成存取的問題

編譯錯誤

error[E0106]: missing lifetime specifier
 --> src/main.rs:6:16
  |
6 | fn dangle() -> &String {
  |                ^ help: consider giving it a 'static lifetime: `&'static`
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

解決方式是修改成直接回傳 String

fn no_dangle() -> String {
    let s = String::from("hello");

    s
} // s 的 ownership 會被 move 出去

The Rules of References

  • 在任意時間點,都只能有一個 mutable reference,要不然就是有多個 immutable refrences
  • references 必須永遠 valid

The Slice Type

另一種沒有 ownership 的資料類別是 slice。slice 可讓我們引用一個 collection 中的連續元素,而不是直接使用整個 collection

sample: 寫一個函數,參數為 String,回傳該 String 找到的第一個 word。如果該 String 中沒有空白,整個字串就是一個 word,也就是要回傳整個 String。

首先嘗試回傳 word 結尾的索引

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    // 將 s 轉換為 byte array,然後用 iter 逐個 char 檢查是不是空白
    // 當遇到第一個空白,就回傳 index i
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    // 整個 string 都沒有空白,就回傳字串長度
    s.len()
}

如果 String 在呼叫 first_word 後,又呼叫了 clear(),該字串會被清空,但 index 卻沒有同時改變。另外這個 function 沒有辦法處理一個開頭就有空白字元的字串。

String Slices

String slice 是 String 中一部分值的引用 [start..end],是由 start 開始,直到 end,但不包含 end 的 range。

如果是 [start..=end],就有包含 end

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

let hello = &s[0..=4];
let world = &s[6..=10];

這是 let world = &s[6..11]; let world = &s[6..=10];的狀況

如果開始是 0 可以省略不寫,如果最後面沒寫,就代表是字串結尾

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

// 以下這兩個是一樣的
let slice = &s[0..len];
let slice = &s[..];

slice range 的索引必須要在有效的 UTF-8 字元邊界內,如果要從 multi-byte 文字中間,建立 slice,會發生錯誤。

剛剛的 first_word 就要改用 slice 回傳,對 slice 字串,呼叫 clear,會發生編譯錯誤。原因是 borrowing 的規則,如果有某個值的可變引用時,就不能再取得另一個可變引用。

fn main() {
    let s = String::from("this is a test");
    let s2 = first_word(&s);

        // compile error!
    //s2.clear();

    println!("s2={}", s2);
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

String Literals Are Slices

let s = "Hello, world!";

這是 string literal,s 實際上的類別是 &str ,是指向程式特定位置的 slice。因此 string literal 是不可變的, &str 是不可變引用。

String Slices as Parameters

剛剛的 first_word

fn first_word(s: &String) -> &str {     

實際上如果改為 &str,就可以對 String&str 使用相同的函數,讓函數更通用

fn first_word(s: &str) -> &str {
fn main() {
    let my_string = String::from("hello world");

    // first_word 中傳入 `String` 的 slice
    let word1 = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word 中傳入字符串字面值的 slice
    let word2 = first_word(&my_string_literal[..]);

    // 因為 string literal 就等於 string slice
    // 不使用 slice 語法這樣寫也可以
    let word3 = first_word(my_string_literal);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

其他類別的 slice

string slice 是針對 string,而 array 也有提供 slice

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

References

The Rust Programming Language

中文版

中文版 2

2020年3月2日

Rust03 Common Programming Concepts

這一章介紹所有程式語言都有的概念,在 Rust 中是如何使用的,包含了 變數、資料型別、Function、Comments、Control Flow。

Variable and Mutability

rust 的變數預設是 immutable,這也是 rust 安全性的基礎。

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    // 編譯錯誤: cannot assign twice to immutable variable
    x = 6;
    println!("The value of x is: {}", x);

    // 宣告時加上 mut,就可以重新綁定 x
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

Constants 常數類似 immutable variable,不能對常數使用 mut,宣告時要用 const 而不是 let

const MAX_POINTS: u32 = 100_000;

Shadowing

可以重新定義相同名稱的變數,前一個變數就會被 shadowing

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("The value of x is: {}", x);
}

使用 shadowing 跟 mut 是有差別的,let 會產生一個新的變數,可以改變該變數的資料型別,但使用相同的變數名稱。但 mut 不能改變該變數的資料型別。

Data Types

rust 中每一個值都有一個 data type,rust 才知道要如何使用該 data。

Rust 是 static type language,編譯時就要確定知道每一個變數的資料型別。編譯器可以推測變數的資料型別,但如果有多種可能時,必須自己指定資料型別。例如 parse,轉換後的結果要是數字的話,就要加上 :u32

let guess: u32 = "42".parse().expect("Not a number!");

scalar

rust 有四種 scalar: integers, floating-point numbers, Booleans, and characters

  • integers: 整數,有號跟無號兩種

    Length Signed Unsigned
    8-bit i8 u8
    16-bit i16 u16
    32-bit i32 u32
    64-bit i64 u64
    128-bit i128 u128
    arch isize usize

    有號數可儲存 \(-(2^{n - 1} )\) 到 \(2^{n - 1} - 1\) 的整數,無號數可儲存 0 到 \(2^n -1\) 的整數

    整數可以在數字後面加上型別的後綴 ex: 57u8 ,也可以加上 _ 分隔符號方便閱讀, ex: 1_000

    Number literals Example
    Decimal 98_222
    Hex 0xff
    Octal 0o77
    Binary 0b1111_0000
    Byte (u8 only) b'A'

integer overflow

​ 假設有個 u8 變數,如果將值修改為 256,就會發生 integer overflow。當 rust 在 debug mode 編譯時,會檢查這個問題,並讓程式 panic。 但是在 release mode,rust 不檢查 integer overflow,反而會進行 two's complement wrapping, 256 就會變成 0,而 257 變成 1。標準函式庫中有一個類別提供此功能: Wrapping

  • floating-point numbers

    rust 有兩種 floating-point numbers 類別: f32, f64。預設是 f64

    fn main() {
        let x = 2.0; // f64
    
        let y: f32 = 3.0; // f32
    }

    浮點數採用 IEEE-754 標準表示。f32 是單精度浮點數,f64 是雙精度浮點數。

  • Numeric Operations

​ 所有數字都支援基本的數學運算: + - * / 以及 % (mod)

     fn main() {
    // 加法
    let sum = 5 + 10;

    // 減法
    let difference = 95.5 - 4.3;

    // 乘法
    let product = 4 * 30;

    // 除法
    let quotient = 56.7 / 32.2;

    // 取餘
    let remainder = 43 % 5;
}

  • Booleans

    true 與 false

    fn main() {
        let t = true;
    
        let f: bool = false;
    }
  • characters

char 代表一個 unicode scalar value,從 U+0000U+D7FFU+E000U+10FFFF 在內的值

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

Compound Types

可將多種型別的資料合併成一個類別, rust 有兩個原生的 compound types: typle, array

tuple

用圓括號加逗點區隔。可用 pattern matching 取得每一個 tuple element 的數值。也可以用 . 加上 index,

ex: x.0

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);

    let (x, y, z) = tup;
    println!("The value of y is: {}", y);

    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}

array

array 中每一個元素的資料型別必須要相同,另外 array 的長度是固定的,宣告後就不能任意加長或縮短。

fn main() {
    let a = [1, 2, 3, 4, 5];
    let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

如果想要在 stack 而不是 heap 儲存資料,或是想要有固定數量的元素時,可使用 array。要不然,就可以使用 vector,vector 類似 array 但允許增長或縮短。

array 的資料型別看起來像是 [type; number]

let a: [i32; 5] = [1, 2, 3, 4, 5];

array 存放在 stack,可用 index 取得元素

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

存取超過 array 長度的 index 會造成程式 panic

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let element = a[index];

    println!("The value of element is: {}", element);
}

編譯沒有錯誤,但執行會出現 runtime error。 這個特性提供了 rust 安全機制,禁止存取異常的 index 的資料。

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/guessing_game`
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:5:19
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Functions

fn 用來宣告函數, main 是程式的入口點

函數跟變數的名稱,使用 snake case 風格,所有字母都是小寫,且用 _ 分隔單字

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

函數可以加上參數 parameters,但實際上應該稱為 arguments。不過大家已經不區分 parameter (定義中的變數) 以及arguments (傳入函數的 value)。

fn main() {
    another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

function bodies 包含 statements & expressions

function bodies 是由一連串的 statements組成,最後面 optional 可加上一個 expression 結束。

Statements: 執行一些操作但不返回值的指令

Expressions : 計算並產生一個值

fn main() {
    // 錯誤,因為 let statement 不會產生回傳 value
    let x = (let y = 6);
}

換句話說,不能用 x=y=6 這樣類似 C, Ruby 的寫法

fn main() {
    let x = 5;

    // { } 之間是一個 block of codes,最後是一個 expression
    // expression 後面不能加上 ;
    // expression 會產生回傳值,並指定給 y
    let y = {
        // 這裡的 x 不會影響到外面的 x
        let x = 3;
        x + 1
    };

    println!("The value of x is: {}, y is: {}", x, y);
}

有 return value 的 function

function 可定義 return value 的資料型別,可使用 return 在函數中間直接回傳,會是在函數最後面,使用 expression。return 後面可加上 ; ,但 expression 後面不能加上 ;

fn test(flag: bool) -> i32 {
    if flag {
        return 4;
    }
    5
}

fn main() {
    let x = test(true);

    println!("The value of x is: {}", x);
}

Comments

// 後面就是註解

fn main() {
    let lucky_number1 = 7; // I’m feeling lucky today

    // I’m feeling lucky today
    let lucky_number2 = 7;
}

Control Flow

loop 跟 if

if expression

Rust 只會執行第一個條件為真的代碼塊,並且一旦它找到一個以後,就不會檢查剩下的條件了。所以這個程式只會列印 number is divisible by 3

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

在 let statement 中使用 if

因 if 是 expression,可以放在 let 的右邊

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    println!("The value of number is: {}", number);
}

但如果 if 跟 else 回傳的資料型別不同,就會發生編譯錯誤

fn main() {
    let condition = true;
    
    // 編譯錯誤
    let number = if condition {
        5
    } else {
        "six"
    };

    println!("The value of number is: {}", number);
}

Loops

rust 有三種循環: loop, while, for

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        // 滿足此條件時, counter 為 10
        if counter == 10 {
            // 以 break 停止循環,並回傳 10 * 2
            break counter * 2;
        }
    };

    assert_eq!(result, 20);
}

while 迴圈,在裡面也可以直接用 break 停止迴圈

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number = number - 1;
    }

    println!("LIFTOFF!!!");
}

可用 while 或是 for,處理 array 中每一個元素

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    // 使用 while 要注意 index,不能超過 array 的長度
    while index < 5 {
        println!("the value is: {}", a[index]);

        index = index + 1;
    }

    // 使用 for 就不需要注意 index
    for element in a.iter() {
        println!("the value is: {}", element);
    }

    // (1..4) 是 Range,可用 rev() 反轉順序
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

References

The Rust Programming Language

中文版

中文版 2

2020年2月23日

Rust Guessing Game

一個基本的猜數字範例程式:程式會產生一個 1 ~ 100 的亂數,提示我們輸入數字猜猜看,填寫數字會得到太大或太小的結果,猜對時就結束遊戲。

Project

首先產生一個空的 guessting game project

$ cargo new guessing_game
     Created binary (application) `guessing_game` package
$ cd guessing_game

因為 src/main.rs 有基本的 hello world 範例,故新的專案都可以直接執行

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/idea/rust/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.39s
     Running `target/debug/guessing_game`
Hello, world!

Processing a Guess

首先要處理 user 輸入的數字

rust 預設只會匯入 prelude,如果沒有在 prelude 就要用 use 匯入該函式庫

// 匯入 io library
use std::io;

// main() 函式是程式的進入點
// fn 語法宣告一個新函式,() 表示沒有傳入的參數,{ 後面開始是函式的內容
// 因為沒有回傳型別,所以這裡的回傳會是 (),一個空的 tuple
fn main() {
    // println! 是列印字串的巨集
    println!("Guess the number!");

    println!("Please input your guess.");

    // let 是變數跟數值的綁定,預設是 immutable
    // 這裡加上 mut,變成 mutable variable
    // String 是 UTF-8 編碼的可變長度字串型別
    let mut guess = String::new();

    // io::stdin() 是使用 std::io::stdin(),會回傳一個 stdin 的 handle
    // 因最前面有 use std:io,所以這裡可省略 std::
    // 呼叫該 handle 的 read_line method,並提供 guess 變數的 reference

    // read_line() 會把使用者的輸入資料放入 &mut String 參數中。而它會回傳一個值 io::Result
    // Rust 的標準函式庫中有很多 Result 的型別:有一般的 Result 以及子函式庫的特別版本,ex: io::Result
    // Result 是要將錯誤訊息編碼
    // Result 的 variants 是 Ok, Err, Ok 裡面有成功的結果, Err 裡面有錯誤的原因

    // io:Result 提供 expect 方法,可取得呼叫該 method 的結果,如果結果不成功,就會 panic! ,並加上後面的 msg 訊息內容
    // 去掉expect 仍然可以編譯程式,但會出現 warning
    io::stdin().read_line(&mut guess).expect("Failed to read line");

    // 列印 guess 變數的資料, {} 是 placeholder
    println!("You guessed: {}", guess);
}

執行

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/idea/rust/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
1
You guessed: 1

Generating a Secret Number

Rust 標準函式庫沒有亂數的功能,但有提供 rand crate,crate 是一包 rust 的程式碼,而 rand 是 library crate,包含了可被其他城市使用的 codes。

要使用 crate 必須修改 Cargo.toml,在 dependencies 裡面加上 rand = "0.4.6"

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["name <email@domain>"]

[dependencies]
rand = "0.4.6"

0.4.6 的部分稱為 Semantic Versioning (有時稱為SemVer),是標準的版本號碼,0.4.6 等同 ^0.4.6,就是任何跟 0.4.6 版本有相容 pulbic API的版本。如果要指定只能使用 0.4.6,要寫成 rand="=0.4.6"

再執行一次,會看到下載了新的 crate library,以及相依的 libc

$ cargo run
    Blocking waiting for file lock on the registry index
    Updating crates.io index
  Downloaded rand v0.3.23
  Downloaded rand v0.4.6
   Compiling libc v0.2.55
   Compiling rand v0.4.6
   Compiling rand v0.3.23
   Compiling guessing_game v0.1.0 (/Users/charley/project/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 20.87s
     Running `target/debug/guessing_game`

cargo 內建了一個機制,確保專案會使用相同版本的 dependencies,第一次建構專案後 (cargo build),就會產生一個 Cargo.lock檔案,該檔案裡面記錄了所有 dependencies 的版本號碼,接下來在重新 build project 時,會先檢查 Cargo.lock,並使用裡面的 versions of the dependencies。

如果要更新 crate 的版本,就要使用 cargo update 指令。

Generating a Random Number

使用 rand::Rng 產生亂數

use std::io;
// Rng 有產生亂數的 methods
use rand::Rng;

fn main() {
    println!("Guess the number!");

    // 1 是下限,101 是 exclusive upper bound
    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

Comparing the Guess to the Secret Number

use std::io;
// Ordering 也是一個 Enum,成員是 Less, Greater, Equal
use std::cmp::Ordering;
use rand::Rng;

fn main() {

    // ---snip---

    println!("You guessed: {}", guess);

    // 把 guess 與 secret_number 做比較
    // 目前這裡會發生 mismatched types 錯誤,因為 Rust 是 strong type language
    // Rust 推測 guess 是 String,而 secret_number 是數字(i32/u32),預設為 i32
    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

編譯錯誤

$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integer
   |
   = note: expected type `&std::string::String`
              found type `&{integer}`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: Could not compile `guessing_game`.

要修改程式,將 guess 轉換為數字

// --snip--

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    // 重新定義 guess,trim 可去掉前後的空白或換行符號
    // 現在 guess 變成 u32,而 Rust 也會將 secret_number 視為 u32
    let guess: u32 = guess.trim().parse()
        .expect("Please type a number!");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}           

加上 loop 重複猜測數字

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse()
            .expect("Please type a number!");

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }

}

目前程式還需要再猜對時,正常跳出程式。另外要針對 guess 的轉型進行檢查。

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    // println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        // 將 expect 換成 match
        // parse 返回一個 Result 類型,而 Result 是一個 Ok 或 Err 成員的 enum
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            },
        }
    }

}

References

The Rust Programming Language

中文版

中文版 2

2020年2月17日

Media Resource Control Protocol - MRCP

MRCP 是 speech server 給 client 提供服務(例如 speech recognition, speech synthesis)的傳輸協定,MRCP 無法獨立運作,必須透過 RTSP 或 SIP 建立 control session 與 audio streams。MRCP 是使用類似 http 的 text style protocol,每個訊息包含三個部分:first line, header, body。

MRCP 使用跟 http 一樣的 a request and reponse model,例如 MRCP client 發送 request,要求要發送 audio data 給 server 做語音辨識,server 會回傳一個訊息,裡面包含要接收資料的 port number,因為 MRCP 並沒有規範要如何傳送語音資料,這部分就透過 RTP 處理。

MRCP v2 (RFC 6787)使用 SIP 管理 session 與 audio stream,v1 (RFC 4463) 則沒有規範這部分要使用哪一種 protocol,目前比較常討論的是 MRCP v2,另外因為MRCPv1依賴 RTSP (RFC2326),但在討論 MRCP v2 時,大家一致 RTSP 的這種使用方式,會導致向後兼容性問題,因此在 (Requirements for Distributed Control of Automatic Speech Recognition (ASR), Speaker Identification/Speaker Verification (SI/SV), and Text-to-Speech (TTS) Resources RFC4313) 的3.2節禁止使用,這就是為什麼MRCPv2不能在RTSP上運作的原因。

MRCP V2 中使用了 SIP 負責建立獨立的媒體和會話支持語音媒體資源,增加了對講話者變化和講話者的身份引擎的支援(speaker verification 和 identification),同時增加了未來的擴充能力。

MRCP v2 規範中的架構圖為

     MRCPv2 client                   MRCPv2 Media Resource Server
|--------------------|            |------------------------------------|
||------------------||            ||----------------------------------||
|| Application Layer||            ||Synthesis|Recognition|Verification||
||------------------||            || Engine  |  Engine   |   Engine   ||
||Media Resource API||            ||    ||   |    ||     |    ||      ||
||------------------||            ||Synthesis|Recognizer |  Verifier  ||
|| SIP  |  MRCPv2   ||            ||Resource | Resource  |  Resource  ||
||Stack |           ||            ||     Media Resource Management    ||
||      |           ||            ||----------------------------------||
||------------------||            ||   SIP  |        MRCPv2           ||
||   TCP/IP Stack   ||---MRCPv2---||  Stack |                         ||
||                  ||            ||----------------------------------||
||------------------||----SIP-----||           TCP/IP Stack           ||
|--------------------|            ||                                  ||
         |                        ||----------------------------------||
        SIP                       |------------------------------------|
         |                          /
|-------------------|             RTP
|                   |             /
| Media Source/Sink |------------/
|                   |
|-------------------|

                      Figure 1: Architectural Diagram

W3C 在 1999年建立 Voice Broswer Working Group(VBWG),研究如何透過 web 支援語音辨識及 DTMF 處理,然後發佈了基於 web 的語音介面架構,核心是 VoiceXML。

W3C 的 Speech Recognition Grammar Specification (SRGS) 是一種 XML 標準,支援語音語法的規則,可識別的短詞語。和 SRGS 比較接近的是 W3C Semantic Interpretation for Speech Recognition (SISR),它更常用在標記語義信息支援語音語法,構成了對自然語言理解的基本格式。W3C Speech Synthesis Markup Language (SSML)是基於 XML 的方式指定內容進行語音合成的方式,可控制語音的各種屬性,包括音量大小,發音,語音間距,語速等方面的控制。

SRGS和SSML能互補和控制W3C的發音語法規則(Pronunciation Lexicon Specification (PLS))。PLS可以使用標準的發音字母來指定單字和短詞語發音。

VoiceXML 協助 MRCP,可支援多種第三方語音辨識及合成引擎。

MRCPv2 Media Resource Types

一個 MRCPv2 server 就是一種 SIP server,因此是用 SIP URI 方式定址 (sip:mrcpv2@example.net or sips:mrcpv2@example.net),可提供以下 media processing resources 給 clients

  • Basic Synthesizer

    透過連接 audio clips 產生語音 media stream,speech data 是以 limited subset of the Speech Synthesis Markup Language (SSML) 描述,最簡單的 synthesizer 必須支援這些 SSML tags: <speak>, <audio>, <say-as>, <mark>

  • Speech Synthesizer

    有完整 TTS 功能,必須完整支援 SSML

  • Recorder

    recoding audio 並提供該錄音的 URI,必須支援在錄音的最前面及後面要 supressing silence,錄音檔的中間可選擇要不要 supress silence,如果有做靜音處理,要記錄 timting metadata,才能知道原始錄音 media 實際發生語音的 timestamp

  • DTMF Recognizer

    能取得 media stream 中的 Dual-Tone Multi-Frequency (DTMF) digits,並對應到 supplied sigit grammar 中

  • Speech Recognizer

    完整的 speech recognition resource 可接收 audio media stream 並辨識取得結果,另外包含一個 natural language semantic interpreter 做辨識結果的 post-process,轉為 grammar 中的 semantic data

  • Speaker Verifier

    可辨別已存在的 voice print 的 speaker

Resource Type Resource Description
speechrecog Speech Recognizer
dtmfrecog DTMF Recognizer
speechsynth Speech Synthesizer
basicsynth Basic Synthesizer
speakverify Speaker Verification
recorder Speech Recorder

MRCPv2 的規範中,整個應用的使用過程如下:

  1. MRCP Client 通過SIP&SDP建立與MRCP Server的MRCP control channel(使用MRCP 通道ID進行唯一標識,MRCP Server返回200消息時,通過a==channel屬性指定)

  2. 可以使用SIP的Re-INVITE消息添加或者刪除一個會話中的MRCP control channel,所以一個 session 可以擁有多個MRCP control channels(比如:一個會話可以同時擁有ASR&TTS channel)

  3. 多個MRCP control channel 可以共享同一個TCP connection

  4. 一個 MRCP message 只能攜帶一個MRCP channel ID。

  5. MRCP控制消息不能更改 SIP dialog 的狀態。

  6. 由於MRCP不保證傳輸的可靠性,所以必須使用TCP/TLS來保證其傳輸

resourse control channel

MRCPv2 附在 SIP 的 SDP 裡面,client 透過 SIP Invite 連接 MRCPv2 server,產生 SIP dialog,SDP 讓兩個端點協調所有要建立的 resource control channel,並產生 server 與 source/sink of audio 之間的 media session。

client 需要建立獨立的 MRCPv2 resource control channel,控制 SIP dialog 裡面要處理的 media resource,因此需要產生一個唯一的 channel identifier string。

在 SDP 中,要有一行 "m=" 給 session 中每一個 MRCPv2 resource 使用,transport type 必須要是 "TCP/MRCPv2" or "TCP/TLS/MRCPv2",client 可透過 TCP 或 TCP/TLS 連接到 MRCPv2 server。

example:

連接到 synthesizer 的範例,server 會產生一個單向 audio stream 傳給 client

  1. 產生 Synthesizer Control Channel
C->S:  INVITE sip:mresources@example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf1
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314161 INVITE
          Contact:<sip:sarvi@client.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=sarvi 2890844526 2890844526 IN IP4 192.0.2.12
          s=-
          c=IN IP4 192.0.2.12
          t=0 0
          m=application 9 TCP/MRCPv2 1
          a=setup:active
          a=connection:new
          a=resource:speechsynth
          a=cmid:1
          m=audio 49170 RTP/AVP 0
          a=rtpmap:0 pcmu/8000
          a=recvonly
          a=mid:1

   S->C:  SIP/2.0 200 OK
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf1;received=192.0.32.10
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314161 INVITE
          Contact:<sip:mresources@server.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=- 2890842808 2890842808 IN IP4 192.0.2.11
          s=-
          c=IN IP4 192.0.2.11
          t=0 0
          m=application 32416 TCP/MRCPv2 1
          a=setup:passive
          a=connection:new
          a=channel:32AECB234338@speechsynth
          a=cmid:1
          m=audio 48260 RTP/AVP 0
          a=rtpmap:0 pcmu/8000
          a=sendonly
          a=mid:1

   C->S:  ACK sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf2
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:Sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314161 ACK
          Content-Length:0

上面的 RTP 資源,另外再對 recognizer 要求取得一個 resource control channel 的資源,並改為 sendrecv 雙向傳輸語音

   C->S:  INVITE sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf3
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314162 INVITE
          Contact:<sip:sarvi@client.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=sarvi 2890844526 2890844527 IN IP4 192.0.2.12
          s=-
          c=IN IP4 192.0.2.12
          t=0 0
          m=application 9 TCP/MRCPv2 1
          a=setup:active
          a=connection:existing
          a=resource:speechsynth
          a=cmid:1
          m=audio 49170 RTP/AVP 0 96
          a=rtpmap:0 pcmu/8000
          a=rtpmap:96 telephone-event/8000
          a=fmtp:96 0-15
          a=sendrecv
          a=mid:1
          m=application 9 TCP/MRCPv2 1
          a=setup:active
          a=connection:existing
          a=resource:speechrecog
          a=cmid:1

   S->C:  SIP/2.0 200 OK
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf3;received=192.0.32.10
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314162 INVITE
          Contact:<sip:mresources@server.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=- 2890842808 2890842809 IN IP4 192.0.2.11
          s=-
          c=IN IP4 192.0.2.11
          t=0 0
          m=application 32416 TCP/MRCPv2 1
          a=setup:passive
          a=connection:existing
          a=channel:32AECB234338@speechsynth
          a=cmid:1
          m=audio 48260 RTP/AVP 0 96
          a=rtpmap:0 pcmu/8000
          a=rtpmap:96 telephone-event/8000
          a=fmtp:96 0-15
          a=sendrecv
          a=mid:1
          m=application 32416 TCP/MRCPv2 1
          a=setup:passive
          a=connection:existing
          a=channel:32AECB234338@speechrecog
          a=cmid:1

   C->S:  ACK sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf4
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:Sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314162 ACK
          Content-Length:0

釋放 recofnizer channel 的資源,改回 recvonly

   C->S:  INVITE sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf5
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314163 INVITE
          Contact:<sip:sarvi@client.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=sarvi 2890844526 2890844528 IN IP4 192.0.2.12
          s=-
          c=IN IP4 192.0.2.12
          t=0 0
          m=application 9 TCP/MRCPv2 1
          a=resource:speechsynth
          a=cmid:1
          m=audio 49170 RTP/AVP 0
          a=rtpmap:0 pcmu/8000
          a=recvonly
          a=mid:1
          m=application 0 TCP/MRCPv2 1
          a=resource:speechrecog
          a=cmid:1


   S->C:  SIP/2.0 200 OK
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf5;received=192.0.32.10
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314163 INVITE
          Contact:<sip:mresources@server.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=- 2890842808 2890842810 IN IP4 192.0.2.11
          s=-
          c=IN IP4 192.0.2.11
          t=0 0
          m=application 32416 TCP/MRCPv2 1
          a=channel:32AECB234338@speechsynth
          a=cmid:1
          m=audio 48260 RTP/AVP 0
          a=rtpmap:0 pcmu/8000
          a=sendonly
          a=mid:1
          m=application 0 TCP/MRCPv2 1
          a=channel:32AECB234338@speechrecog
          a=cmid:1

   C->S:  ACK sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf6
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:Sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314163 ACK
          Content-Length:0

MRCPv2 message

MRCPv2 訊息包含 client 給 server 的 request,及server 發給 client 的 response 與asynchronous events,資料格式包含一行 start-line,多個 headers,一行 empty line 代表 header 結束,然後是 optional message body,跟 http protocol 類似

generic-message  =    start-line
                      message-header
                      CRLF
                      [ message-body ]

message-body     =    *OCTET

start-line       =    request-line / response-line / event-line

message-header   =  1*(generic-header / resource-header / generic-field)

resource-header  =    synthesizer-header
                 /    recognizer-header
                 /    recorder-header
                 /    verifier-header

ex:

   C->S:   MRCP/2.0 877 INTERPRET 543266
           Channel-Identifier:32AECB23433801@speechrecog
           Interpret-Text:may I speak to Andre Roy
           Content-Type:application/srgs+xml
           Content-ID:<request1@form-level.store>
           Content-Length:661

           <?xml version="1.0"?>
           <!-- the default grammar language is US English -->
           <grammar xmlns="http://www.w3.org/2001/06/grammar"
                    xml:lang="en-US" version="1.0" root="request">
           <!-- single language attachment to tokens -->
               <rule id="yes">
                   <one-of>
                       <item xml:lang="fr-CA">oui</item>
                       <item xml:lang="en-US">yes</item>
                   </one-of>
               </rule>
           <!-- single language attachment to a rule expansion -->
               <rule id="request">
                   may I speak to
                   <one-of xml:lang="fr-CA">
                       <item>Michel Tremblay</item>
                       <item>Andre Roy</item>
                   </one-of>
               </rule>
           </grammar>

   S->C:   MRCP/2.0 82 543266 200 IN-PROGRESS
           Channel-Identifier:32AECB23433801@speechrecog

   S->C:   MRCP/2.0 634 INTERPRETATION-COMPLETE 543266 200 COMPLETE
           Channel-Identifier:32AECB23433801@speechrecog
           Completion-Cause:000 success
           Content-Type:application/nlsml+xml
           Content-Length:441

           <?xml version="1.0"?>
           <result xmlns="urn:ietf:params:xml:ns:mrcpv2"
                   xmlns:ex="http://www.example.com/example"
                   grammar="session:request1@form-level.store">
               <interpretation>
                   <instance name="Person">
                       <ex:Person>
                           <ex:Name> Andre Roy </ex:Name>
                       </ex:Person>
                   </instance>
                   <input>   may I speak to Andre Roy </input>
               </interpretation>
           </result>

request-line 的格式為

   request-line   =    mrcp-version SP message-length SP method-name SP request-id CRLF

   method-name    =    generic-method
                  /    synthesizer-method
                  /    recognizer-method
                  /    recorder-method
                  /    verifier-method
                  
   request-id     =    1*10DIGIT

response 的格式為

response-line  =    mrcp-version SP message-length SP request-id
                       SP status-code SP request-state CRLF
status-code     =    3DIGIT
request-state    =  "COMPLETE"
                    /  "IN-PROGRESS"
                    /  "PENDING"

event-line 的格式為

event-line       =  mrcp-version SP message-length SP event-name
                       SP request-id SP request-state CRLF
event-name       =  synthesizer-event
                    /  recognizer-event
                    /  recorder-event
                    /  verifier-event

注意到訊息格式中,分別對 synthesizer, recognizer, recorder, verifier 四種 resource type,有不同的定義 methods, headers, events

Generic Methods, Headers, Result Structure

所有 resource 通用的 methods, headers

MRCPv2 支援兩種 generic methods,可 reading, writing 相關資源的 state

   generic-method      =    "SET-PARAMS"
                       /    "GET-PARAMS"

SET-PARAMS

​ client 發送給 server,通知該 session 的 MRCPv2 resource 要定義 parameter

   C->S:  MRCP/2.0 ... SET-PARAMS 543256
          Channel-Identifier:32AECB23433802@speechsynth
          Voice-gender:female
          Voice-variant:3

   S->C:  MRCP/2.0 ... 543256 200 COMPLETE
          Channel-Identifier:32AECB23433802@speechsynth

GET-PARAMS

​ client 發送給 server,通知要取得 MRCPv2 resource 目前的 session parameters

   C->S:   MRCP/2.0 ... GET-PARAMS 543256
           Channel-Identifier:32AECB23433802@speechsynth
           Voice-gender:
           Voice-variant:
           Vendor-Specific-Parameters:com.example.param1;
                         com.example.param2
   S->C:   MRCP/2.0 ... 543256 200 COMPLETE
           Channel-Identifier:32AECB23433802@speechsynth
           Voice-gender:female
           Voice-variant:3
           Vendor-Specific-Parameters:com.example.param1="Company Name";
                         com.example.param2="124324234@example.com"

所有 MRCPv2 header 中,包含 generic-headers 及 resource-specific headers

header 的定義如下

   generic-field  = field-name ":" [ field-value ]
   field-name     = token
   field-value    = *LWS field-content *( CRLF 1*LWS field-content)
   field-content  = <the OCTETs making up the field-value
                    and consisting of either *TEXT or combinations
                    of token, separators, and quoted-string>

generic header 有

   generic-header      =    channel-identifier
                       /    accept
                       /    active-request-id-list
                       /    proxy-sync-id
                       /    accept-charset
                       /    content-type
                       /    content-id
                       /    content-base
                       /    content-encoding
                       /    content-location
                       /    content-length
                       /    fetch-timeout
                       /    cache-control
                       /    logging-tag
                       /    set-cookie
                       /    vendor-specific
  • Channel-Identifier

    在產生一個 control channel 時,由 server 指定一個 Channel Id

   channel-identifier  = "Channel-Identifier" ":" channel-id CRLF
   channel-id          = 1*alphanum "@" 1*alphanum
  • Accept

  • Active-Request-Id-List

    在 request 裡面,這個 header 代表這個 request 對這個 list of request-ids 有作用。在 response ,這個 header 代表該 method 影響到的 list of request-ids

       active-request-id-list  =  "Active-Request-Id-List" ":"
                                  request-id *("," request-id) CRLF
  • Proxy-Sync-Id

    當某個 server resource 產生 "barge-in-able" event,也會產生一個 unique tag,該 tag 會透過這個 header 放在 event 裡面,傳給 client

       proxy-sync-id    =  "Proxy-Sync-Id" ":" 1*VCHAR CRLF
  • Accept-Charset

    在 request 裡面指定 response or event 可接受能夠處理的 character sets。

    例如指定 Natural Language Semantic Markup Language (NLSML) results 的 RECOGNITION-COMPLETE event 可使用的 character set

  • Content-Type

    MRCPv2 的 content 支援有限 media types,例如 speech markup, grammer, recofnition results

       content-type     =    "Content-Type" ":" media-type-value CRLF
    
       media-type-value =    type "/" subtype *( ";" parameter )
    
       type             =    token
    
       subtype          =    token
    
       parameter        =    attribute "=" value
    
       attribute        =    token
    
       value            =    token / quoted-string
  • Content-ID

    該 content 參考或引用的 ID or name

  • Content-Base

    指定 base URI

    content-base      = "Content-Base" ":" absoluteURI CRLF
  • Content-Encoding

    某個 Content-Type 的附加資訊,例如 Content-Encoding:gzip

       content-encoding  = "Content-Encoding" ":"
                           *WSP content-coding
                           *(*WSP "," *WSP content-coding *WSP )
                           CRLF
  • Content-Location

       content-location  =  "Content-Location" ":"
                            ( absoluteURI / relativeURI ) CRLF
  • Content-Length

    message body 的長度

       content-length  =  "Content-Length" ":" 1*19DIGIT CRLF
  • Fetch Timeout

    當 recognizer/synthesizer 需要取得文件或其他資源,定義 server 透過網路取得資源的 timeout 時間

       fetch-timeout       =   "Fetch-Timeout" ":" 1*19DIGIT CRLF
  • Cache-Control

    如果 server 有支援 content caching,遵循 http 1.1 的規則提供 cache

       cache-control    =    "Cache-Control" ":"
                             [*WSP cache-directive
                             *( *WSP "," *WSP cache-directive *WSP )]
                             CRLF
    
       cache-directive     = "max-age" "=" delta-seconds
                           / "max-stale" [ "=" delta-seconds ]
                           / "min-fresh" "=" delta-seconds
    
       delta-seconds       = 1*19DIGIT
  • Logging-Tag

    SET-PARAMS/GET-PARAMS method 的 header,可 set/retrieve server 產生的 log 的 logging tag

       logging-tag    = "Logging-Tag" ":" 1*UTFCHAR CRLF
  • Set-Cookie

    類似 http 的 cookie,讓 server 在 client 存放 cookie values

  • Vendor-Specific Parameters

    ex:

       com.example.companyA.paramxyz=256
       com.example.companyA.paramabc=High
       com.example.companyB.paramxyz=Low

Generic Result Structure

Recognizer 與 Verifier resource server 產生的 result data,以 Natural Language Semantics Markup Language (NLSML) 格式提供

ex:

   Content-Type:application/nlsml+xml
   Content-Length:...

   <?xml version="1.0"?>
   <result xmlns="urn:ietf:params:xml:ns:mrcpv2"
           xmlns:ex="http://www.example.com/example"
           grammar="http://theYesNoGrammar">
       <interpretation>
           <instance>
                   <ex:response>yes</ex:response>
           </instance>
           <input>OK</input>
       </interpretation>
   </result>

Resource Discovery

透過 SIP OPTIONS 向 server 詢問 server capabilities

server 必須以 SDP 回應 capabilities,包含 media type, transport type: m=application 0 TCP/TLS/MRCPv2 1,以及 resource: a=resource:speechsynth

ex:

   C->S:
        OPTIONS sip:mrcp@server.example.com SIP/2.0
        Via:SIP/2.0/TCP client.atlanta.example.com:5060;
         branch=z9hG4bK74bf7
        Max-Forwards:6
        To:<sip:mrcp@example.com>
        From:Sarvi <sip:sarvi@example.com>;tag=1928301774
        Call-ID:a84b4c76e66710
        CSeq:63104 OPTIONS
        Contact:<sip:sarvi@client.example.com>
        Accept:application/sdp
        Content-Length:0


   S->C:
        SIP/2.0 200 OK
        Via:SIP/2.0/TCP client.atlanta.example.com:5060;
         branch=z9hG4bK74bf7;received=192.0.32.10
        To:<sip:mrcp@example.com>;tag=62784
        From:Sarvi <sip:sarvi@example.com>;tag=1928301774
        Call-ID:a84b4c76e66710
        CSeq:63104 OPTIONS
        Contact:<sip:mrcp@server.example.com>
        Allow:INVITE, ACK, CANCEL, OPTIONS, BYE
         Accept:application/sdp
        Accept-Encoding:gzip
        Accept-Language:en
        Supported:foo
        Content-Type:application/sdp
        Content-Length:...

        v=0
        o=sarvi 2890844536 2890842811 IN IP4 192.0.2.12
        s=-
        i=MRCPv2 server capabilities
        c=IN IP4 192.0.2.12/127
        t=0 0
        m=application 0 TCP/TLS/MRCPv2 1
        a=resource:speechsynth
        a=resource:speechrecog
        a=resource:speakverify
        m=audio 0 RTP/AVP 0 3
        a=rtpmap:0 PCMU/8000
        a=rtpmap:3 GSM/8000

Speech Synthesizer Resource

client 發送 text markup,讓server 即時產生 audio stream,可指定語音合成的參數,例如 voice characteristics, speaker speed

有兩種: speech synth, basicsynth

Synthesizer State Machine

pending 的 SPEAK request 可以被 deleted/stopped

   Idle                    Speaking                  Paused
   State                   State                     State
     |                        |                          |
     |----------SPEAK-------->|                 |--------|
     |<------STOP-------------|             CONTROL      |
     |<----SPEAK-COMPLETE-----|                 |------->|
     |<----BARGE-IN-OCCURRED--|                          |
     |              |---------|                          |
     |          CONTROL       |-----------PAUSE--------->|
     |              |-------->|<----------RESUME---------|
     |                        |               |----------|
     |----------|             |              PAUSE       |
     |    BARGE-IN-OCCURRED   |               |--------->|
     |<---------|             |----------|               |
     |                        |      SPEECH-MARKER       |
     |                        |<---------|               |
     |----------|             |----------|               |
     |         STOP           |       RESUME             |
     |          |             |<---------|               |
     |<---------|             |                          |
     |<---------------------STOP-------------------------|
     |----------|             |                          |
     |     DEFINE-LEXICON     |                          |
     |          |             |                          |
     |<---------|             |                          |
     |<---------------BARGE-IN-OCCURRED------------------|

Synthesizer Methods

   synthesizer-method   =  "SPEAK"
                        /  "STOP"
                        /  "PAUSE"
                        /  "RESUME"
                        /  "BARGE-IN-OCCURRED"
                        /  "CONTROL"
                        /  "DEFINE-LEXICON"

Synthesizer Events

   synthesizer-event    =  "SPEECH-MARKER"
                        /  "SPEAK-COMPLETE"

Synthesizer Header Fields

   synthesizer-header  =  jump-size
                       /  kill-on-barge-in
                       /  speaker-profile
                       /  completion-cause
                       /  completion-reason
                       /  voice-parameter
                       /  prosody-parameter
                       /  speech-marker
                       /  speech-language
                       /  fetch-hint
                       /  audio-fetch-hint
                       /  failed-uri
                       /  failed-uri-cause
                       /  speak-restart
                       /  speak-length
                       /  load-lexicon
                       /  lexicon-search-order

Example:

text 會被合成並播放到 media stream,resource 會產生 IN-PROGRESS, SPEAK-COMPLETE event

   C->S: MRCP/2.0 ... SPEAK 543257
         Channel-Identifier:32AECB23433802@speechsynth
         Voice-gender:neutral
         Voice-Age:25
         Prosody-volume:medium
         Content-Type:application/ssml+xml
         Content-Length:...

         <?xml version="1.0"?>
            <speak version="1.0"
                xmlns="http://www.w3.org/2001/10/synthesis"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://www.w3.org/2001/10/synthesis
                   http://www.w3.org/TR/speech-synthesis/synthesis.xsd"
                xml:lang="en-US">
            <p>
             <s>You have 4 new messages.</s>
             <s>The first is from Stephanie Williams and arrived at
                <break/>
                <say-as interpret-as="vxml:time">0342p</say-as>.
                </s>
             <s>The subject is
                    <prosody rate="-20%">ski trip</prosody>
             </s>
            </p>
           </speak>

   S->C: MRCP/2.0 ... 543257 200 IN-PROGRESS
         Channel-Identifier:32AECB23433802@speechsynth
         Speech-Marker:timestamp=857206027059

   S->C: MRCP/2.0 ... SPEAK-COMPLETE 543257 COMPLETE
         Channel-Identifier:32AECB23433802@speechsynth
         Completion-Cause:000 normal
         Speech-Marker:timestamp=857206027059

Speech Recognizer Resource

接收 client 提供的 voice stream,轉換為文字

有兩種: speechrecog, dtmfrecog

recognizer resource 的能力有:

  1. Normal Mode Recognition:會將整個語音或 DTMF 判斷是否吻合

  2. Hotword Mode Recognition

    判斷是否有出現某個特定的 speech grammar or DTMF sequence

  3. Voice Enrolled Grammars

    (optional) enrollment 是用某個人的 voice 進行判斷, server 會維護 a list of contacts,包含人員的名稱以及 voice,這個技術也稱為 speaker-dependent recognition

  4. Interpretation

    natural language interpretation

    以 text 作為 input,產生該文字的 grammar

Recognizer State Machine

   Idle                   Recognizing               Recognized
   State                  State                     State
    |                       |                          |
    |---------RECOGNIZE---->|---RECOGNITION-COMPLETE-->|
    |<------STOP------------|<-----RECOGNIZE-----------|
    |                       |                          |
    |              |--------|              |-----------|
    |       START-OF-INPUT  |       GET-RESULT         |
    |              |------->|              |---------->|
    |------------|          |                          |
    |      DEFINE-GRAMMAR   |----------|               |
    |<-----------|          | START-INPUT-TIMERS       |
    |                       |<---------|               |
    |------|                |                          |
    |  INTERPRET            |                          |
    |<-----|                |------|                   |
    |                       |   RECOGNIZE              |
    |-------|               |<-----|                   |
    |      STOP                                        |
    |<------|                                          |
    |<-------------------STOP--------------------------|
    |<-------------------DEFINE-GRAMMAR----------------|

Recognizer Methods

   recognizer-method    =  recog-only-method
                        /  enrollment-method
   recog-only-method    =  "DEFINE-GRAMMAR"
                        /  "RECOGNIZE"
                        /  "INTERPRET"
                        /  "GET-RESULT"
                        /  "START-INPUT-TIMERS"
                        /  "STOP"
   enrollment-method    =  "START-PHRASE-ENROLLMENT"
                        /  "ENROLLMENT-ROLLBACK"
                        /  "END-PHRASE-ENROLLMENT"
                        /  "MODIFY-PHRASE"
                        /  "DELETE-PHRASE"

Recognizer Events

   recognizer-event     =  "START-OF-INPUT"
                        /  "RECOGNITION-COMPLETE"
                        /  "INTERPRETATION-COMPLETE"

Recognizer Header Fields

   recognizer-header    =  recog-only-header
                        /  enrollment-header

   recog-only-header    =  confidence-threshold
                        /  sensitivity-level
                        /  speed-vs-accuracy
                        /  n-best-list-length
                        /  no-input-timeout
                        /  input-type
                        /  recognition-timeout
                        /  waveform-uri
                        /  input-waveform-uri
                        /  completion-cause
                        /  completion-reason
                        /  recognizer-context-block
                        /  start-input-timers
                        /  speech-complete-timeout
                        /  speech-incomplete-timeout
                        /  dtmf-interdigit-timeout
                        /  dtmf-term-timeout
                        /  dtmf-term-char
                        /  failed-uri
                        /  failed-uri-cause
                        /  save-waveform
                        /  media-type
                        /  new-audio-channel
                        /  speech-language
                        /  ver-buffer-utterance
                        /  recognition-mode
                        /  cancel-if-queue
                        /  hotword-max-duration
                        /  hotword-min-duration
                        /  interpret-text
                        /  dtmf-buffer-time
                        /  clear-dtmf-buffer
                        /  early-no-match
                        
   enrollment-header    =  num-min-consistent-pronunciations
                        /  consistency-threshold
                        /  clash-threshold
                        /  personal-grammar-uri
                        /  enroll-utterance
                        /  phrase-id
                        /  phrase-nl
                        /  weight
                        /  save-best-waveform
                        /  new-phrase-id
                        /  confusable-phrases-uri
                        /  abort-phrase-enrollment

Example

   C->S:MRCP/2.0 ... RECOGNIZE 543257
   Channel-Identifier:32AECB23433801@speechrecog
           Confidence-Threshold:0.9
   Content-Type:application/srgs+xml
   Content-ID:<request1@form-level.store>
   Content-Length:...

   <?xml version="1.0"?>

   <!-- the default grammar language is US English -->
   <grammar xmlns="http://www.w3.org/2001/06/grammar"
            xml:lang="en-US" version="1.0" root="request">

   <!-- single language attachment to tokens -->
       <rule id="yes">
               <one-of>
                     <item xml:lang="fr-CA">oui</item>
                     <item xml:lang="en-US">yes</item>
               </one-of>
         </rule>

   <!-- single language attachment to a rule expansion -->
         <rule id="request">
               may I speak to
               <one-of xml:lang="fr-CA">
                     <item>Michel Tremblay</item>
                     <item>Andre Roy</item>
               </one-of>
         </rule>

     </grammar>

   S->C: MRCP/2.0 ... 543257 200 IN-PROGRESS
   Channel-Identifier:32AECB23433801@speechrecog

   S->C:MRCP/2.0 ... START-OF-INPUT 543257 IN-PROGRESS
   Channel-Identifier:32AECB23433801@speechrecog

   S->C:MRCP/2.0 ... RECOGNITION-COMPLETE 543257 COMPLETE
   Channel-Identifier:32AECB23433801@speechrecog
   Completion-Cause:000 success
   Waveform-URI:<http://web.media.com/session123/audio.wav>;
                 size=424252;duration=2543
   Content-Type:application/nlsml+xml
   Content-Length:...
   <?xml version="1.0"?>
   <result xmlns="urn:ietf:params:xml:ns:mrcpv2"
           xmlns:ex="http://www.example.com/example"
           grammar="session:request1@form-level.store">
       <interpretation>
           <instance name="Person">
               <ex:Person>
                   <ex:Name> Andre Roy </ex:Name>
               </ex:Person>
           </instance>
               <input>   may I speak to Andre Roy </input>
       </interpretation>
   </result>

Recorder Resource

將收到的 audio/video 存到指定的 URI

Recorder State Machine

   Idle                   Recording
   State                  State
    |                       |
    |---------RECORD------->|
    |                       |
    |<------STOP------------|
    |                       |
    |<--RECORD-COMPLETE-----|
    |                       |
    |              |--------|
    |       START-OF-INPUT  |
    |              |------->|
    |                       |
    |              |--------|
    |    START-INPUT-TIMERS |
    |              |------->|
    |                       |

Recorder Methods

   recorder-method      =  "RECORD"
                        /  "STOP"
                        /  "START-INPUT-TIMERS"

Recorder Events

   recorder-event       =  "START-OF-INPUT"
                        /  "RECORD-COMPLETE"

Recorder Header Fields

   recorder-header      =  sensitivity-level
                        /  no-input-timeout
                        /  completion-cause
                        /  completion-reason
                        /  failed-uri
                        /  failed-uri-cause
                        /  record-uri
                        /  media-type
                        /  max-time
                        /  trim-length
                        /  final-silence
                        /  capture-on-speech
                        /  ver-buffer-utterance
                        /  start-input-timers
                        /  new-audio-channel

example

   C->S:  MRCP/2.0 ... RECORD 543257
          Channel-Identifier:32AECB23433802@recorder
          Record-URI:<file://mediaserver/recordings/myfile.wav>
          Media-Type:audio/wav
          Capture-On-Speech:true
          Final-Silence:300
          Max-Time:6000

   S->C:  MRCP/2.0 ... 543257 200 IN-PROGRESS
          Channel-Identifier:32AECB23433802@recorder

   S->C:  MRCP/2.0 ... START-OF-INPUT 543257 IN-PROGRESS
          Channel-Identifier:32AECB23433802@recorder

   S->C:  MRCP/2.0 ... RECORD-COMPLETE 543257 COMPLETE
          Channel-Identifier:32AECB23433802@recorder
          Completion-Cause:000 success-silence
          Record-URI:<file://mediaserver/recordings/myfile.wav>;
                     size=242552;duration=25645

Speaker Verification and Identification

辨識 speaker 的身份

Speaker Verification State Machine

     Idle              Session Opened       Verifying/Training
     State             State                State
      |                   |                         |
      |--START-SESSION--->|                         |
      |                   |                         |
      |                   |----------|              |
      |                   |     START-SESSION       |
      |                   |<---------|              |
      |                   |                         |
      |<--END-SESSION-----|                         |
      |                   |                         |
      |                   |---------VERIFY--------->|
      |                   |                         |
      |                   |---VERIFY-FROM-BUFFER--->|
      |                   |                         |
      |                   |----------|              |
      |                   |  VERIFY-ROLLBACK        |
      |                   |<---------|              |
      |                   |                         |
      |                   |                |--------|
      |                   | GET-INTERMEDIATE-RESULT |
      |                   |                |------->|
      |                   |                         |
      |                   |                |--------|
      |                   |     START-INPUT-TIMERS  |
      |                   |                |------->|
      |                   |                         |
      |                   |                |--------|
      |                   |         START-OF-INPUT  |
      |                   |                |------->|
      |                   |                         |
      |                   |<-VERIFICATION-COMPLETE--|
      |                   |                         |
      |                   |<--------STOP------------|
      |                   |                         |
      |                   |----------|              |
      |                   |         STOP            |
      |                   |<---------|              |
      |                   |                         |
      |----------|        |                         |
      |         STOP      |                         |
      |<---------|        |                         |
      |                   |----------|              |
      |                   |    CLEAR-BUFFER         |
      |                   |<---------|              |
      |                   |                         |
      |----------|        |                         |
      |   CLEAR-BUFFER    |                         |
      |<---------|        |                         |
      |                   |                         |
      |                   |----------|              |
      |                   |   QUERY-VOICEPRINT      |
      |                   |<---------|              |
      |                   |                         |
      |----------|        |                         |
      | QUERY-VOICEPRINT  |                         |
      |<---------|        |                         |
      |                   |                         |
      |                   |----------|              |
      |                   |  DELETE-VOICEPRINT      |
      |                   |<---------|              |
      |                   |                         |
      |----------|        |                         |
      | DELETE-VOICEPRINT |                         |
      |<---------|        |                         |

Speaker Verification Methods

   verifier-method          =  "START-SESSION"
                            / "END-SESSION"
                            / "QUERY-VOICEPRINT"
                            / "DELETE-VOICEPRINT"
                            / "VERIFY"
                            / "VERIFY-FROM-BUFFER"
                            / "VERIFY-ROLLBACK"
                            / "STOP"
                            / "CLEAR-BUFFER"
                            / "START-INPUT-TIMERS"
                            / "GET-INTERMEDIATE-RESULT"

Verification Events

   verifier-event       =  "VERIFICATION-COMPLETE"
                        /  "START-OF-INPUT"

Verification Header Fields

   verification-header      =  repository-uri
                            /  voiceprint-identifier
                            /  verification-mode
                            /  adapt-model
                            /  abort-model
                            /  min-verification-score
                            /  num-min-verification-phrases
                            /  num-max-verification-phrases
                            /  no-input-timeout
                            /  save-waveform
                            /  media-type
                            /  waveform-uri
                            /  voiceprint-exists
                            /  ver-buffer-utterance
                            /  input-waveform-uri
                            /  completion-cause
                            /  completion-reason
                            /  speech-complete-timeout
                            /  new-audio-channel
                            /  abort-verification
                            /  start-input-timers

References

MRCP wiki

MRCP協議學習筆記-MRCP背景知識介紹

MRCP學習筆記-語音識別資源的事件和Headers詳解

MRCP協議學習筆記-語音識別資源的概括和全部Methods

MRCP協議學習筆記-關於媒體資源伺服器的定位路由策略

MRCPv2概述

MRCPv2在電信智能語音識別業務中的應用

MRCPv2 - Speech Synthesizer Resource

MRCP v2.0 規範 - RFC6787中文翻譯(1)

cisco 使用MRCPv1 ASR/TTS的IOS語音XML網關到CVP呼叫流