読書メモ: プログラミング言語 Rust 公式ガイド
The Rust Programming Language (日本語版) の邦訳版で ASCII DWANGO から出版されている「プログラミング言語 Rust 公式ガイド」の読書メモ。レベルとしては1つ2つの汎用プログラミング言語を使いこなすことができるプログラマ向けの入門書で、特に C/C++ と関数型言語を使える人ならすんなり読める内容だろう。
Table of Contents
- 第一章 事始め
- 第二章 数当てゲームをプログラムする
- 第三章 一般的なプログラミングの概念
- 第四章 所有権を理解する
- 第五章 構造体を使用して関係のあるデータを構造化する
- 第六章 enum とパターンマッチング
- 第七章 モジュールを使用してコードを体系化し、再利用する
- 第八章 一般的なコレクション
- 第九章 エラー処理
- 第十章 ジェネリック型、トレイト、ライフタイム [under construction]
- 第十一章 自動テストを書く
- 第十二章 入出力プロジェクト: コマンドラインプログラムを構築する
- 第十三章 関数型言語の機能: イテレーターとクロージャー
- 第十四章 Cargo と crates.io についてより詳しく
- 第十五章 スマートポインター
- 第十六章 恐れるな! 並行性
- 第十七章 Rust のオブジェクト指向プログラミング機能
- 第十八章 パターンとマッチング
- 第十九章 高度な機能
- 第二十章 最後のプロジェクト: マルチスレッドの Web サーバを構築する
- 参考文献
本書に書かれている言語構造や言語機能を手元で試すために Rust 版 REPL をインストールしておくと利便性が良い。
$ cargo install evcxr_repl
Updating crates.io index
Downloaded evcxr_repl v0.4.5
...
$ evcxr
Welcome to evcxr. For help, type :help
>> println!("hello, world")
hello, world
()
>>
第一章 事始め
Rust のインストールと hello, world プログラムのコンパイル、cargo を使用したビルド方法などの紹介。
- Rust の安定性保証によりこの本に掲載されているサンプルコードは新しい Rust のバージョンになってもコンパイルでき続けることが保証されている。
Windows で Rust 開発コマンドを使用するには Visual Studio 2013 以降の C++ ビルドツールが必要。手っ取り早い方法は Visual Studio 2019 Community をインストールしてしまうこと。VS2019 のインストールが完了し再起動を行ったら Install Rust から
rustup-init.exe
をダウンロードして実行する。それ以外にも (Windows バイナリにはならないが) Windows Subsystem for Linux を使用して Linux と同じ方法でコンパイル環境を構築することができる。
rustup update
で最新版へ更新できる。Rust と rustup のアンインストールはrustup self uninstall
で行う。rustc --version
,cargo --version
でそれぞれバージョンが表示される。- hello, world プログラムのコンパイルと実行。
> type hello_world.rs fn main(){ println!("hello, world"); } > rustc hello_world.rs > hello_world.exe hello, world
- Cargo は Rust のビルドシステム兼パッケージマネージャ。Scala の sbt や Ruby の gem に相当。プログラムが依存するライブラリを
Cargo.toml
に記述しておくと Cargo が暗黙的にダウンロードし参照できる状態で実行することができる。cargo new [project]
で新しいプロジェクトディレクトリとCargo.toml
が生成される。 - Cargo コマンドは
build
でビルド、run
で実行。check
はバイナリ生成の処理時間を省略してコンパイル可能かのみをチェックする。--release
オプションを追加するとリリースビルドになる。
第二章 数当てゲームをプログラムする
標準入力から入力した数値で乱数を当てるゲームを使って依存ライブラリの追加方法や extern crate
, use
による名前解決、標準入出力の使い方、変数・関数の書き方などを一通り解説している。
- (今のバージョンでは分からないが) 第1版の翻訳時にはソースコードのコメント以外の部分に日本語の文字を使用するとコンパイルに失敗することがあったとのこと。JDK 0.9 時代を思い出す。なのでサンプルコードで使用するメッセージなどの文字列リテラルは英語のままにしているとのこと。
prelude
に定義されているいくつかの型だけはuse
を使用しなくても使うことができる。let
のみは不変の変数であるためスコープ中に値を書き換えることができない。可変の変数を使用したい場合はlet mut
を使用する。プログラムの安全性の観点からなるべく不変の変数を使用することが勧められるが、巨大なデータ構造の一部のみを書き換えるような場合はコピーを生成するより可変にして直接書き換えたほうがパフォーマンスが良い。- シャドーイング (shadowing) を使用して不変の変数を
let
で再定義することができる。これは内部的には同名の別の変数に代入していることと同じ。したがって型が変わっても問題ない。 - 失敗する可能性のある関数などの結果には一般的に
Result
を返す設計を行う。これは Scala などの Either と同じ。Ok
とErr
の enum 列挙であり、Ok
は成功したことを表しErr
はその失敗理由などを保持している (ということはErr
は個別の状態を持つことになり列挙値ではなく型ではないか?)。io::Result
といったResult
を派生させたライブラリ固有の定義も多く存在する (固有のエラー情報を追加するため?) - 標準の
println!("x := {}", x)
は{}
でプレースホルダーを使用できる。 Cargo.toml
に記述する[dependencies]
のバージョン指定は「そのバージョンの互換性のある任意のバージョン」という意味を持つ。つまり "0.3.14" と指定したとき、実際には "0.3.21" が使われるかもしれないが "0.4.2" が使われることはない。Specifying dependencies from crates.io によればデフォルトのこの動作は "^0.3.14" と指定したのと等価。"~0.3.14" と指定すると "0.4.2" が使われる可能性がある。ワイルドカードとして "0.3.*" とすることもできる。Cargo.lock
は実際にその環境で使用している特定のバージョンを保存している。ほかの人の端末で上位バージョンが動作しなかった場合の調査に使用することができる。cargo update
で最新版に更新可能。- 外部クレートはソース内で
extern crate lib_name;
とするとuse
のトップレベルで使用することができる。 - 文字列の数値化は対象の整数型が推論できないとコンパイルエラーとなるため、型指定した変数に代入
let x = t.parse();
するか、t.parse::<u32>()
のように型を明示したメソッドを呼び出す。 - パターンマッチ
match A { X => ... Y => ... }
やloop
-break/continue
構文。
第三章 一般的なプログラミングの概念
Rust の言語機能に関するさわりを説明している。
const
キーワードにより定数を宣言することができる。このとき型は必ず指定しなければならない。定数はどのようなスコープでも定義することができる。定数式しか設定することができない。つまり関数の呼び出し結果や実行時に評価される値は設定できない。命名規則はすべて大文字でアンダースコアによる単語区切り。let
で定義した変数であってもシャドーイングで再定義することができる。シャドーイングは型が変わっても良いため中間結果を一時保存するようなケースでも使える。例えば:let spaces = " "; let spaces = spaces.len();
- 整数型の中でも
isize
,usize
はターゲットプラットフォームのポインタサイズを示す。ターゲットプラットフォームとはコンパイル環境か、コンパイル時に指定した実行プラットフォームの環境か、それとも実行環境ごとに 32/64bit 差異を同一バイナリで吸収してくれるものなのかは不明。 - 整数型、浮動小数点型、論理値型、文字型が利用できる。複合型にタプル型、配列型が利用できる。配列は常に固定長の要素を持ちヒープではなくスタックに保存される。可変長を扱いたい場合はベクター型を使う。配列のインデックス外参照は実行時エラーを起こす。
- 関数において引数の型宣言は必須。
- 関数名で early return する場合は
return
を使用するが、一般的に関数の最後はreturn
を付けず評価結果を返値とする。このために最後の式にはセミコロンを記述しない (セミコロンをつけると()
を返すことになる)。 - ブロック
{...}
やif
は式であり値を返すことができる。
繰り返し命令>> let x = 100; >> let y = if x > 100 { 101 } else { 99 }; >> println!("x={}, y={}", x, y); x=100, y=99
loop
,while
,for-in
は通常の言語と同様。結果は Unit となる。>> let mut i = 0; >> loop { if i > 2 { break; } println!("i={}", i); i += 1 } i=0 i=1 i=2 () >> let mut i = 0; >> loop { if i > 2 { break; } println!("i={}", i); i += 1 } i=0 i=1 i=2 () >> let mut i = 0; >> while i <= 2 { println!("i={}", i); i += 1 } () i=0 i=1 i=2 >> for i in 0..3 { println!("i={}", i); } i=0 i=1 i=2 ()
第四章 所有権を理解する
- 所有権の規則
- すべての値は「所有者」となる変数に対応している。
- すべてのタイミングで所有者は 1 人。
- 所有者の変数がスコープから外れたら値は破棄される。
- Rust の文字列は文字列リテラル (
&str
; 文字配列のスライス) とString
型の 2 種類を使うことができる。String
は可変宣言で Java のStringBuilder
のように扱うことができる。 - 実体の所有者がスコープを外れ値が破棄される前に
drop()
関数が呼ばれる。これは C++ のデストラクタや Java のファイナライザのような言語機能。 - スタックに配置された単純な整数などは代入によって新しい変数に値がコピーされる。構造体の代入は shallow copy され所有権が新しい変数に移り以前の変数は無効となる (ムーブ)。所有者が必ず 1 人となることで二重解放が発生しなくなる。
- ディープコピーは
clone()
で提供されている。 Copy
トレイトに適合していれば値のコピーの挙動となり、代入後も以前の変数を使い続けることができる。単純なスカラー値はCopy
に適合している。構造体のようなメモリ確保が必要なものはCopy
にはなっていない。- 関数の引数渡しでも代入と同様に所有権が移る。呼び出し元に所有権を維持したままにしたい場合は参照で渡すことができる。
- 参照が生きている間はその実体の所有者も生きていなければならない。したがって関数内で作成した実態を参照として返すことはできない。この場合は実体のムーブとして返すことができる。
&mut
とすると可変な参照となる。不変な参照はいくらでも作ることができるが、可変な参照はあるタイミングで一つしか作ることができない。これは不変を前提に参照している値が変化しないようにする措置。- ダングリングとは既に所有権が移り、別の所有者によって drop されてしまったポインタを持ち続けていること。Rust はダングリングが発生しないことを保証する。
- スライスは実体そのものへの参照ではないが同等の参照規則が適用される。例えばスライスの不変参照のスコープ中に可変操作を行おうとするとコンパイルエラーが発生する。
- 文字列リテラル
&str
はシステム的には文字配列のスライスの不変参照。文字列プールを参照しているような概念? - マルチバイト文字の中間をスライスで分離しようとすると落ちる(?)らしい。
第五章 構造体を使用して関係のあるデータを構造化する
構造体 struct
とメソッド impl
に関する使い方の簡単な説明。
- 構造体は JSON の Object と似た書式で定義する。構築時にフィールドの初期化を行う。フィールド定義の末尾のコンマはあってもなくてもよい。
immutable の手段があるとはいえフィールドは常に丸見えか? 構築を隠ぺいするような方法はないのか?>> struct Product{ name:String, price:u32 } >> let p = Product{ name:String::from("pencil"), price:130 };
- フィールド名とローカル変数名が同じときにフィールド初期化の省略表記を使うことができる。
ローカル変数名に依存した挙動は違和感あり。コンストラクタ的な関数を用意するときに多少楽かもしれないが。fn f(name:String, price:u32) -> Product { Product { name, price } }
- あるインスタンスの一部のフィールドだけを書き換えた別のインスタンスを作成することができる。Scala での case class の
copy()
に相当。>> let p1 = Product{ name: String::from("pencil"), price: 130 }; >> let p2 = Product{ name: String::from("eraser"), ..p1 };
- 構造体のフィールド名はなくてもよい。この場合、名前付きのタプルのようになる。
>> struct XY(f32, f32); >> let o = XY(0.0, 0.0);
- トレイトの実装だけに使用するため、フィールドを持たない構造体を定義することができる。ユニット構造体と呼ばれる。
Debug
トレイトを継承するとprintln!()
の"{:?}"
,"{:#?}"
プレースホルダーがすべてのフィールドの出力に成形される。>> #[derive(Debug)] struct Product { name:String, price:u32 } >> let p = Product{ name:String::from("pencil"), price:130 }; >> println!("product={:?}", p) product=Product { name: "pencil", price: 130 }: >> println!("product={:#?}", p); product=Product { name: "pencil", price: 130, }
- 構造体に対するメソッドは
impl [struct-name]{ ... }
で定義する。インスタンスメソッドの第一引数は&self
とし型は自明であるため省略できる。メソッド内で特定のフィールドを更新する必要がある場合は&mut self
とする。>> #[derive(Debug)] struct Product { name:String, price:u32 } >> let p = Product{ name:String::from("pencil"), price:130 }; >> impl Product { fn priceIncludesTax(&self) -> u32 { (self.price as f64 * 1.1) as u32 } } >> p.priceIncludesTax() 143 >> impl Product { fn raise(&mut self, diff:u32){ self.price += diff; } } >> let mut q = Product { name: String::from("eraser"), price: 150 }; >> q.raise(20); >> q.price 170
- C/C++ でのメンバーアクセスは変数が実体かポインタかで
.
と->
を分けていたが Rust では参照の場合は暗黙的に(*foo).bar()
と等価と解釈される。 - インスタンスを変数に取らないメソッドは関連関数と呼ばれる。C++ や Java における静的メソッドと等価。
>> impl Product{ fn tax(price:u32) -> u32 { (price as f64 * 0.1) as u32 } } >> Product::tax(100) 10
- 1 つの構造体に複数の
impl
を定義することができる。Go や Julia と同じく構造的部分型の言語設計。
第六章 enum とパターンマッチング
- Rust の enum は単なる定数値の列挙だけではなく代数データ型を定義するために使用することができる。列挙子として (定数のように使用できる) ユニット様構造体、タプル構造体、通常の構造体を使用することができる。
#[derive(Debug)] enum Json { Null, String(String), Int(i32), Boolean(bool), Array(Vec<Json>), Object(Vec<(String,Json)>), } >> let s = Json::String(String::from("hello, world")); >> let i = Json::Int(1024); >> let arr = Json::Array(vec![Json::String(String::from("A")), Json::Int(256), Json::Null]); >> let obj = Json::Object(vec![(String::from("str"), Json::String(String::from("B"))), (String::from("int"), Json::Int(64))]); >> println!("s={:?}, i={:?}, arr={:?}, obj={:?}", s, i, arr, obj); s=String("hello, world"), i=Int(1024), arr=Array([String("A"), Int(256), Null]), obj=Object([("str", String("B")), ("int", Int(64))])
#[derive(Debug)] enum Tree<T> { Leaf(T), Node{ left:Box<Tree<T>>, right:Box<Tree<T>> } }
- 代数データ型を使用した
Option<T>
,Some(T)
,None
がデフォルトで用意されている。目的や使い方は Scala や Java のそれと同じ。 - 代数データ型のパターンマッチは Scala と同様に extractor としても機能する。包括的マッチが強制される。「その他」を表すパターン
_
が使用できる。
extractor で得られるのは参照? ライフタイムはどうなってる?>> match Some(String::from("foo")) { Some(msg) => println!("msg={}", msg), None => println!("None") } msg=foo
if let
を用いてパターンマッチ代入を行うことができる。Scala では非包括的マッチでも記述できたが Rust ではコンパイルエラーとなるためif let
を使う必要がある。>> if let Some(bar) = Some(String::from("foo")) { println!("{}", bar) } foo
第七章 モジュールを使用してコードを体系化し、再利用する
- モジュールは関数や型定義を含む名前空間のこと。
mod
キーワードでモジュールを作成する。- モジュールやそれに含まれる関数、型、定数は非公開。
pub
キーワードを使用して何を公開するかを選ぶことができる。 - 名前空間を解決するために
use
キーワードを使用することができる。
クレートにはバイナリクレートとライブラリクレートがあり、それぞれ
src/
下のmain.rs
かlib.rs
のどちらをエントリポイントとして参照するかで異なる(cargo new
で新規クレートを作成するときに--lib
を指定するとライブラリクレートが作成される)。混在は可能だがバイナリクレートはextern crate
を使用してライブラリクレートを参照しなければならない。ライブラリクレートのビルドはcargo build
を使用する。$ cargo new foo --lib $ cat foo/src/lib.rs #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } }
バイナリクレートにモジュールは作成できるのか?
ライブラリの実装は
lib.rs
にすべてのコードを書くことができるが通常はmod
を使ってモジュール分けをする。mod foo;
と実体のないモジュールを定義したとき、コンパイラはサブモジュールの実装がfoo.rs
かfoo/mod.rs
に存在するものとして参照する。$ cat src/lib.rs pub mod a; pub mod b; $ cat src/a.rs pub const PI:f64 = 3.14159265359; pub fn circle_area(radius:f64) -> f64 { radius * radius * PI } $ cat src/b/mod.rs pub enum Shape { Point, Line, Circle, Rectangle, } pub mod c; $ cat src/b/c.rs pub fn rect_area(width:f64, height:f64) -> f64 { width * height }
この構成だとサブモジュールに含まれるコードは一つのファイルに記述可能な範囲であることが望まれ、それより大きい場合は非公開のサブモジュールに分割して関数だけを公開するような構造を強いるだろう。本当?
- 公開性の規則。
- 要素が公開されている場合はどのモジュールからも利用が可能。
- 要素が非公開の場合は直属の親か要素の兄弟からのみ利用が可能。
use
は名前空間の解決に使用する。モジュールだけではなくenum
にも使用できる。- トップレベルから絶対位置指定で名前空間を指定する場合は
::foo::bar
のように記述する。相対位置指定で親を指定する場合はsuper::foo:bar
のように記述する。名前空間内のすべてを参照する場合はfoo::*
のように記述が可能で*
は glab 演算子 と呼ぶ。
第八章 一般的なコレクション
ベクター操作
- Rust ではジェネリクスを利用できる。可変長配列の役割となるベクター型
let Vec<i32> = Vec::new();
は要素となる値の型を指定する。ただし初期値を指定する場合は型推論が機能するためlet v = vec!{1, 2, 3};
のように省略可能。 - ベクターのようなコレクションがドロップされるタイミングでその要素もドロップされる (これは要素の所有権をコレクションが持っていることを暗に意味している)。コレクションの内容を変更するには
mut
宣言をする必要がある。 - ベクターの要素はインデックスで
&v[i]
と直接参照するか、Option
を返すv.get(i)
かが使用できる。直接参照で範囲外のインデックスを指定するとパニックを起こす。 - ベクターから要素の列挙は
for in
で行う。要素の状態を変更する必要がある場合はfor x in &mut v { ... }
のように記述する。 - 総称型に
enum
を使用することができる。この効果は Scala でsealed trait
とそのサブクラスであるcase class
,case object
を使って設計するのと同じ。
文字列操作
- 文字列は言語として
str
型 (通常は借用の&str
で使われる) と標準ライブラリで提供されるSring
型がある。 - Rust の標準文字列は UTF-8 で扱うがシステム境界用の
OSString
やCString
も用意されていてその限りではない。 +
演算子を用いて文字列の連結を行うことができる。ただし先頭以外は借用で渡す必要があり、連結後は先頭がムーブされる。
または>> let a = String::from("A"); >> let b = String::from("B"); >> let c = String::from("C"); >> let abc = a + &b + &c; >> println!("{}", abc); ABC >> println!("{}", a); ^ cannot find value `a` in this scope help: a local variable with a similar name exists >> let a = String::from("A"); >> let abc = a + &b + "C"; >> println!("{}", abc); ABC
format!()
を使用すればムーブや借用を意識する必要はない。>> let a = String::from("A"); >> let abc = format!("{}{}{}", a, b, c); >> println!("{}", abc); ABC
String
型は単なるVec<u8>
のラッパー。従ってそのlen()
は UTF-8 バイト数となり、マルチバイト文字に対しては文字数を意味しない。また[]
を使った添え字アクセスもコンパイルエラーとなる。
配列やスライスのインデックス参照は \(O(1)\) が期待されるが、文字列の場合は先頭からすべてを走査しなければならないため \(O(n)\) となる。>> "ABC".len() 3 >> "あいう".len() 9 >> "あいう"[2] ^^^^^^^^ string indices are ranges of `usize` the type `str` cannot be indexed by `{integer}` help: the trait `std::slice::SliceIndex<str>` is not implemented for `{integer}`
chars()
で文字列挙、bytes()
でバイト列挙を取得できる。>> "あいう".chars() Chars(['あ', 'い', 'う'])
マップ操作
- デフォルトでは使用できないので
use std::collection::HashMap;
宣言する必要がある。
またタプルの列挙に対してlet mut m = HashMap::new(); m.insert(String::from("A"), 10); println!("{:#?}", m); // {"A": 10}
collect()
で集約することでも生成することができる。>> (0..5).map(|x| (x.to_string(), x)).collect::<HashMap<String,i32>>(); {"3": 3, "1": 1, "0": 0, "4": 4, "2": 2}
- マップにキーや値を挿入したときの所有権の移動は代入操作と同じ。
Copy
トレイトを実装していれば値がコピーされるが、そうでなければムーブとなり所有権が移動する。>> let mut m:HashMap<String,i32> = HashMap::new(); >> let a = String::from("A"); >> m.insert(a, 1); >> a ^ cannot find value `a` in this scope help: a local variable with a similar name exists >> m.insert(String::from("B"), 2); >> m.insert(String::from("C"), 3); >> m {"A": 1, "B": 2, "C": 3} >> m.get(&String::from("A")) Some(1) >> m.get(&String::from("X")) None >> m.entry(String::from("X")).or_insert(100); >> m {"A": 1, "B": 2, "C": 3, "X": 100}
- デフォルトの
HashMap
は暗号論的ハッシュ関数を使用している。このハッシュアルゴリズムはBuildHasher
トレイトを実装すれば変更することができる。
第九章 エラー処理
Rust のエラーの考え方は 1) 問題をユーザに報告し処理を再実行することのできる回復可能エラーと 2) バグの兆候として表れる回復不能エラーの 2 種類が存在する。Rust には例外が存在せず、回復可能なエラーを報告する Result
と回復不能で処理を中止する panic!
マクロを使用する。
panic!
で回復不能なエラー
panic!
マクロはスタックを巻き戻してプログラムを終了する。「スタックを巻き戻す」とは想定されるすべてのdrop()
を実行して必要な終了手順とリソース開放を行った上でという意味? またマルチスレッド環境で panic! を実行するとどうなる?- 単一のスレッドだけが中断されスレッドが巻き戻される。
- 全てのスレッドが中断されスレッドが巻き戻される。
Cargo.toml
でpanic = abort
を設定するとpanic!
時にスタックの巻き戻しは行わず即時異常終了するようになる。この場合、実行バイナリのサイズが少し小さくなる。リリースビルドのみ異常終了を有効にするには以下のように記述する:[profile.release] panic = 'abort'
RUST_BACKTRACE=1 cargo run
のように実行時に環境変数RUST_BACKTRACE
を設定するとpanic
のバックトレースが出力される。ただし--release
付きでビルドすると表示のためのデバッグシンボルは削除される。
Result
で回復可能なエラー
- プログラムを終了するほどではないときは
Result
型 (Either
モナドとして機能する) を使用する。
パターンマッチ、マッチガードを使用することができる。パターンのlet x = match result_of_something { Ok(x) => x, Err(ref e) if e.kind() == ... => { // リカバリー処理 }, Err(e) => { panic!("something wrong: {:?}", e) }, }
ref
はe
がガード条件式にムーブされないために必要。 - 結果の判定を細分化しない簡易的な手段として
unwrap()
とexpect()
を使用することができる。unwrap()
はErr
の時にパニックを発生させる。expect()
も同様だがエラーメッセージを指定することができる。 - 呼び出し先の関数で発生した
Err
はそのまま呼び出し元に返したいケースの簡易的な手段として?
演算子を使用することができる。
Scala でのfn g(x:f64) -> Result<f64, String> { if x >= 0.0 { Ok(x.sqrt()) } else { Err("negative".to_string()) } } fn f(x:f64, y:f64) -> Result<f64, String> { let xx = g(x)?; let yy = g(y)?; Ok(xx * yy) } fn main(){ print!("{:?}\n", f(1.0, 1.0)); // Ok(1.0) print!("{:?}\n", f(0.0, 0.0)); // Ok(0.0) print!("{:?}\n", f(-1.0, 1.0)); // Err("negative") }
Either.flatMap{...}
に相当。
第十章 ジェネリック型、トレイト、ライフタイム
第十一章 自動テストを書く
第十二章 入出力プロジェクト: コマンドラインプログラムを構築する
第十三章 関数型言語の機能: イテレーターとクロージャー
第十四章 Cargo と crates.io についてより詳しく
第十五章 スマートポインター
第十六章 恐れるな! 並行性
第十七章 Rust のオブジェクト指向プログラミング機能
第十八章 パターンとマッチング
第十九章 高度な機能
第二十章 最後のプロジェクト: マルチスレッドの Web サーバを構築する
参考文献
- Steve Klabnik and Carol Nichols (2018) The Rust Programming Language (Covers Rust 2018)