Rust に入門した
RDBMS 自作のネタをやりたいがために入門してみた。
開発環境: https://youtu.be/677kcyyPwJ4
入門: https://tourofrust.com/chapter_1_ja.html
執筆者の KOBA789 さんの記事 https://diary.hatenablog.jp/entry/2021/04/08/190000
他、参照できそうなリソース
- Zenn Rust 入門 https://zenn.dev/mebiusbox/books/22d4c1ed9b0003
- とほほのRust入門 https://www.tohoho-web.com/ex/rust.html
- Rust Forge https://forge.rust-lang.org/index.html
Rust のツールセットを入れる
$ rustup show
Default host: x86_64-apple-darwin
rustup home: /Users/xxxx/.rustup
stable-x86_64-apple-darwin (default)
rustc 1.51.0 (2fd73fabe 2021-03-23)
なんか色々入ってることはわかった
$ ls ~/.cargo/bin
cargo cargo-fmt clippy-driver rust-gdb rustc rustfmt
cargo-clippy cargo-miri rls rust-lldb rustdoc rustup
rustup は処理系そのものに関するツールのインストーラ、cargo は npm とか pipenv 的なやつ? rustc はコンパイラ ... くらいの認識
$ rustup
rustup 1.24.1 (a01bd6b0d 2021-04-27)
The Rust toolchain installer
USAGE:
rustup [FLAGS] [+toolchain] <SUBCOMMAND>
# ... つづく
The Rust toolchain installer
ということらしい
$ cargo
Rust's package manager
USAGE:
cargo [+toolchain] [OPTIONS] [SUBCOMMAND]
# ... つづく
Rust's package manager
らしい
プロジェクトの始め方は cargo new
とか cargo create
あたりでいいのだろうか
これによれば new している。
$ cargo new rust_hello
Created binary (application) `rust_hello` package
$ code rust_hello
プロジェクト作成して、VSCode で開く
動画のおすすめに従って、 rust-analyzer, clippy をインストール
rust-clippy: https://github.com/rust-lang/rust-clippy
rust-analyzer の Check On Save: Command
に clippy を指定
とりあえず開発環境はOKとして、チュートリアルをやる
これをある程度飛ばし飛ばしで見ていく
変数宣言は let
を使う。 mut
という修飾子っぽいワードをつければ mutable な値になるらしい
基本は immutable なのか?? ↓実際に Playground で試したらそうっぽい
fn main() {
let mut x = 42;
println!("{}", x);
x = 13;
println!("{}", x);
let y = 20;
y = 10;
println!("{}", y);
}
エラーメッセージ(一部抜粋)は
8 | y = 10;
| ^^^^^^ cannot assign twice to immutable variable
immutable が基本なのはよさそう
プリミティブは若干自分が知ってる他の言語よりバリエーションがある
符号なし整数 ... u8, u32, u64, u128
符号付き整数 ... i8, i32, i64, i128
浮動小数点 ... f32, f64
ポインタサイズ整数型 ... usize, isize // 後で使い方調べる
タプルと配列は コンパイル時に 長さが決まるらしい。 immutable が基本だとそうなるか、と納得
一方で実行時に長さを決められる型も存在する。スライス型や、 str はそうらしい。
数値型は型を明示しないといけないらしい。異なる精度の型を式で混ぜるとエラーになる。キャストとして as
キーワードを使える。
fn main() {
let a = 13u8;
let b = 7u32;
let c = a as u32 + b;
println!("{}", c);
let t = true;
println!("{}", t as u8);
}
↑は正しく動作する。ダウンキャストはできるのだろうか??
fn main() {
let a = 13u8;
let b = 7u32;
let c = a + b as u8;
println!("{}", c);
let t = true;
println!("{}", t as u8);
}
こっちも動いた。では、8bit に収まらない数値ではどうか?
fn main() {
let a = 13u8;
let b = 500u32; // u8 へのダウンキャストに収まらない数値
let c = a + b as u8;
println!("{}", c);
let t = true;
println!("{}", t as u8);
}
これはコンパイルエラーが出た。
error: this arithmetic operation will overflow
--> src/main.rs:4:13
|
4 | let c = a + b as u8;
| ^^^^^^^^^^^^ attempt to compute `13_u8 + 244_u8`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default
error: aborting due to previous error
const を使うと定数になる。このへんの構文は C 言語っぽい。
大文字のスネークケースを使い、型は明示する。
const SOME_CONST_VALUE: f32 = 3.14;
配列は [T; N]
という型で定義する
let arr: [i32; 3] = [1, 2, 3]; // 長さ3 の、 i32 型を要素にもつ配列
fn main() {
let nums: [i32; 3] = [1, 2, 3];
println!("{:?}", nums); // [1, 2, 3]
println!("{}", nums[1]);
}
配列のインデックスに見えるやつ [1]
はれっきとした演算子の扱いらしい。ここでの 1
は、 usize 型のインデックス らしい。i8, u8 とかの整数型とは別物(なんでわざわざ??)
"{:?}"
というフォーマットを指定すれば不定長の型も print できるらしい。
Python では _str_, _repr_ などの特殊メソッドがこの辺のオブジェクト型の標準出力をコントロールしてたが、 rust ではそのへんはどうなるのだろうか。
関数定義は python のそれに近い
fn add(x: i32, y: i32) -> i32 {
return x + y;
}
タプルも返せる
fn swap(x: i32, y: i32) -> (i32, i32) {
return (y, x);
}
戻り値の型が指定されていない 場合は unit と呼ばれる空のタプルを返す。(void とはどう違う?)
if, else, else if, while あたりは特に驚きはないので飛ばす。
for は ruby っぽい書き方ができるぽい
fn main() {
for x in 0..5 {
println!("{}", x); // 4まで出力する
}
for x in 0..=5 {
println!("{}", x); // 5まで出力する
}
}
python の range とは違って、stop で指定した数字を含むようにループすることもできる
イテレータの概念は rust にも存在して、ここでの 0..5
や 0..=5
の書き方はイテレータを生成する
while (true)
に相当する構文として loop
というのが使える。他にも使い方があるらしいが、後で說明するらしい
switch 文に相当することをやりたい場合は match が使える
fn main() {
let x = 42;
match x {
0 => {
println!("found zero");
}
// 複数の値にマッチ
1 | 2 => {
println!("found 1 or 2!");
}
// 範囲にマッチ
3..=9 => {
println!("found a number 3 to 9 inclusively");
}
// マッチした数字を変数に束縛
matched_num @ 10..=100 => {
println!("found {} number between 10 to 100!", matched_num);
}
// どのパターンにもマッチしない場合のデフォルトマッチが必須
_ => {
println!("found something else!");
}
}
}
たぶん、他の言語でパターンマッチとか呼ばれる類のやつと思われる。ためしに、束縛しているマッチより手前にマッチさせてみる。おそらく、最初にマッチしたブロックのみ実行されると予想。
fn main() {
let x = 5;
match x {
0 => {
println!("found zero");
}
// 複数の値にマッチ
1 | 2 => {
println!("found 1 or 2!");
}
// 範囲にマッチ
3..=9 => {
println!("found a number 3 to 9 inclusively"); // ここだけが実行された。 found a number 3 to 9 inclusively
}
// マッチした数字を変数に束縛
matched_num @ 10..=100 => {
println!("found {} number between 10 to 100!", matched_num);
}
// どのパターンにもマッチしない場合のデフォルトマッチが必須
_ => {
println!("found something else!");
}
}
}
結果は予想通り。
おそらく、タプルや配列などをマッチさせることもできるし、束縛時に展開することもできるはず。そのへんは後から出てくるものと期待する
セミコロンのあるなしは意味が異なるらしい。ブロックの最後にセミコロンを付けない場合、その最後の式がブロックの戻り値になる。
fn example() -> i32 {
let x = 42;
// Rust の三項式
let v = if x < 42 { -1 } else { 1 };
println!("if より: {}", v);
let food = "ハンバーガー";
let result = match food {
"ホットドッグ" => "ホットドッグです",
// 単一の式で値を返す場合、中括弧は省略可能
_ => "ホットドッグではありません",
};
println!("食品の識別: {}", result);
let v = {
// ブロックのスコープは関数のスコープから分離されている
let a = 1;
let b = 2;
a + b
};
println!("ブロックより: {}", v);
// Rust で関数の最後から値を返す慣用的な方法
v + 4
}
三項演算子っぽい表現もこの構文でできてるのが面白い。
let x = 42;
// Rust の三項式
let v = if x < 42 { -1 } else { 1 };
struct は C 言語の通りっぽいので特にコメントしない。
メソッドの概念は、とりあえず static/instance method で呼び出し方が異なる、ということだけ覚える
25: https://tourofrust.com/25_ja.html
メモリの扱いは C 言語(というか Linux process のメモリ領域)のそれなので特にコメントしない
きりがなさそうなので、実践的なネタを押さえるために実際の例題を見てみる
当面やりたいのはこれ↓
Result あたりがよくわかってない。あと、 unwarup
も。
ざっと見た感じでは、Result 型をちゃんと処理したかったら match で Ok/Err を判別するし、楽するなら unwwap
という選択肢もあり得るっぽい
参考になりそうなのはこのへん?
&mut
みたいな記述があって、これがよくわからない。
- 変数を束縛(代入)すると元の変数はアクセスできなくなる
- 参照は幾つでも作れる
- mutableは一つだけ
1 は move
という概念と関連しているっぽい。代入し直すと代入前に使ってた変数から move した、という整理になるっぽい。
2 は参照渡しなら move は起きないよと言っているらしい。例えば、
let x = vec![1,2,3];
let x2 = &x;
println!("{:?}", x);
このように x が使える(値渡しの場合はエラー)。だいぶ C のポインタに近い。
3 は、まあ複数の場所から値が変更されないような機構を言語レベルで組み込んでおけば安全でしょう、という思想だと理解する。
let x = vec![1,2,3];
let x2 = &x;
let x3 = &mut x; // mutable で渡す
println!("{:?}", x);
と、参照渡しの書き方に &mut
が入ると渡した先で変更が可能になるっぽい。これができるのは1箇所だけ。
簡易 HTTP サーバーの実装では、ストリームから入力を取り込むためにバッファ変数を &mut
で渡していた。このへん、関数のインタフェースはかなり C のそれに近い(引数に結果の変数を渡している感じが)のを押さえていれば変数を mutable 参照で渡す必要性が腑に落ちる
例題に出てきたエクスクラメーションは一体何なのか??
let x = vec![1, 2, 3];
never 型の概念はいまひとつ有用性がわからない。TypeScript にも同様の型があり、たぶんモチベは似ているはず・・・。
それはそれとして、 println!
のような関数名のエクスクラメーションに関しては単なる名前ではないかという感じもする.... → マクロの名前は必ずそうなる、という話らしい。 vec!
や println!
に関してはこっちに該当する話で、上記の never 型とは別物らしい
マクロについて調べてみたついでに、 try!
というマクロが Result の取り回しに関連している雰囲気だったので調べた
// The preferred method of quick returning Errors
fn write_to_file_question() -> Result<(), MyError> {
let mut file = File::create("my_best_friends.txt")?;
file.write_all(b"This is a list of my best friends.")?;
Ok(())
}
// The previous method of quick returning Errors
fn write_to_file_using_try() -> Result<(), MyError> {
let mut file = r#try!(File::create("my_best_friends.txt"));
r#try!(file.write_all(b"This is a list of my best friends."));
Ok(())
}
// This is equivalent to:
fn write_to_file_using_match() -> Result<(), MyError> {
let mut file = r#try!(File::create("my_best_friends.txt"));
match file.write_all(b"This is a list of my best friends.") {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
}
Ok(())
}
いくつか書きようがあることはわかった。とりあえずは match で支障なさそう。1つ目の ?
を使う方が簡潔に見える
Result 関係の話と、早期リターンについて調べる
早期リターンを知るには unwrap の理解も必要そうだったので、一緒に
↑このドキュメント見てると、 ?
は panic(回復不可能なエラー)の可能性をあえて無視して簡潔さを保つ、というモチベらしい。また、 try ではなく ? が今は推奨である とも書かれているのでここは覚えておきたい。そして、 ? と unwrap はかなり近い立ち位置のものらしいこともわかった
unwrap() は、 Option<T> 型や Result<T, E> 型の値(つまり、何かしらの値を ラップ している値)から中身の値を取り出す関数です。たとえば Option<T> 型の値に対して unwrap() を呼ぶと、それが内包する T 型の値を返します。
unwrap() は失敗するかもしれないことに注意が必要です。 Option<T> 型や Result<T, E> 型などの値は、 T 型の値が入っていることもあれば入っていないこともあります。入っていない場合に unwrap() を呼ぶとプログラムは panic します。
なんとなくわかったような気はする
動画の題材で使っていた TcpListener
関係でやっていたことが、ちょっとわかってきた。
use std::net::{TcpListener, TcpStream};
fn handle_client(stream: TcpStream) {
// ...
}
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:80")?;
// accept connections and process them serially
for stream in listener.incoming() {
handle_client(stream?);
}
Ok(())
}
bind は
pub fn bind<A: ToSocketAddrs>(addr: A) -> Result<TcpListener>
と Result 型を返す。上記のサンプルコードにおける記述は、 unwrap
または ?
を使うことによって、直接内包している TcpListener
型の値を得ようとしていることがわかる。
listener.incoming()
がよくわからないので整理する。
定義は以下だが、最後の型がよくわからない。あと、 &self
というのも謎(こっちは python における self みたいなもの...??)
pub fn incoming(&self) -> Incoming<'_>
for 文で回していることや、ドキュメントの
Returns an iterator over the connections being received on this listener.
という記述から、戻り値の Incoming<'_>
はどうやらイテレータらしいとわかる。
<'_>
という表記は、anonymous lifetime と言うらしいがよくわからない。ライフタイムという概念はジェネリックと一緒に紹介されてたが、いったん理解は棚上げする
動画やドキュメントをあさりつつ、なんとか(警告は出るけど)意図したように HTTP レスポンスを返してくれる実装ができた
use std::io::{Read, Write};
use std::error::Error;
use std::net::{TcpListener, TcpStream};
fn handle_request(stream: &mut TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer);
println!("Request: {}", String::from_utf8_lossy(&buffer[..]));
let contents = String::from("<p>hello world</p>");
let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);
stream.write(response.as_bytes());
stream.flush();
}
fn main() {
let listener = TcpListener::bind("0.0.0.0:9000").unwrap();
for stream in listener.incoming() {
let mut st = stream.unwrap();
handle_request(&mut st);
// handle_request に切り出す前の実装。いい感じには動かない
// let mut buffer = [0; 1024];
// st.read(&mut buffer);
// let response = String::from("HTTP/1.1 200 OK");
// let resopnse_body = format!("{}\r\n\r\nHello rust.", response);
// println!("{:?}\n", String::from_utf8_lossy(&buffer));
// st.write(resopnse_body.as_bytes());
// st.flush();
}
}
TCP で簡易 HTTP サーバーを作ってみるにあたって、このへんも参考になりそう
以下は、読んでてほほー、となった記述。
bind関数の引数はToSocketAddrsというトレイトを継承しているオブジェクトですが、上のような「IPアドレス+ポート番号」も受け付けてくれます
TcpListener::bind("0.0.0.0:33333").expect("Error. failed to bind.");
expectはResult<T, E>から中身を取り出し、それがOkだった場合はその値を返し、Errだった場合引数の文字列とErrの値を表示してパニックを起こします。
その他、後から見つけたサンプルコード
今後の方向性として、 note の記事を写経して echo サーバー作ってみるのもいいし、自作の Python 製 backlog api ラッパー を Rust 移植してみるのも良いかもしれない。
また、いくつか、実戦レベルの開発をやるにあたって押さえたいトピックはある
- Testing
- モジュール分割
この辺学べる題材があれば良さそう。
この scrap の意図である「入門してみた」に関しては、上記のどれかを追記するかも。いったんは本来やりたいことである RDBMS 自作の方に舵を切ることにする
雑多な参考文献
演算子と記号
開発環境
エラー処理の考え方
ググりにくい文法に関するメモ
Rust のモジュールシステム
自分でコードを書くにあたって履修必須なので。
ひとまずは後者、 The Rust Programming Language 日本語版 をベースに読んでみる
私達がこれまでに書いてきたプログラムは、一つのファイル内の一つのモジュール内にありました。 プロジェクトが大きくなるにつれて、これを複数のモジュールに、ついで複数のファイルに分割することで、プログラムを整理することができます。 パッケージは複数のバイナリクレートからなり、またライブラリクレートを1つもつこともできます。 パッケージが大きくなるにつれて、その一部を抜き出して分離したクレートにし、外部依存とするのもよいでしょう。 この章ではそれらのテクニックすべてを学びます。 相互に関係し合い、同時に成長するパッケージの集まりからなる巨大なプロジェクトには、 Cargoがワークスペースという機能を提供します。これは14章のCargoワークスペースで解説します。
ワークスペース、とかいう概念があるらしいことだけ覚えておく。
より重要そうなのは、以下
Rustには、どの詳細を公開するか、どの詳細を非公開にするか、どの名前がプログラムのそれぞれのスコープにあるか、といったコードのまとまりを保つためのたくさんの機能があります。 これらの機能は、まとめて「モジュールシステム」と呼ばれることがあり、以下のようなものが含まれます。
- パッケージ: クレートをビルドし、テストし、共有することができるCargoの機能
- クレート: ライブラリか実行可能ファイルを生成する、木構造をしたモジュール群
- モジュール と use: これを使うことで、パスの構成、スコープ、公開するか否かを決定できます
- パス: 要素(例えば構造体や関数やモジュール)に名前をつける方法
クレートがビルド単位に焦点を当てた概念、パッケージはプロジェクト全体ぽい、モジュールやパスがパッケージ(あるいはクレート?)を構成する部分概念なのかな?という印象
cargo new すると src/main.rs
というソースが最初からいるが、これは cargo が src/main.rs を ルートクレートとしてみなしている 仕様によるらしい
ルートクレートとは
Rustコンパイラの開始点となり、クレートのルートモジュールを作るソースファイルのことです(モジュールについて詳しくは「モジュールを定義して、スコープとプライバシーを制御する」のセクションで説明します)
らしい。npm init したときに main のデフォルトが index.js になる、みたいな話だと思う...
パッケージはクレートを複数包含する概念。パッケージは、 Cargo.toml
というビルド設定に関する情報を持っている。
また、パッケージから見たクレートとのつながりは↓のような決まりがあるらしい
パッケージが何を持ってよいかはいくつかのルールで決まっています。 パッケージは0個か1個のライブラリクレートを持っていないといけません。それ以上は駄目です。 バイナリクレートはいくらでも持って良いですが、少なくとも(ライブラリでもバイナリでも良いですが)1つのクレートを持っていないといけません。
ライブラリクレート/バイナリクレート、という新キャラが登場した。 cargo new
のオプションあたりでこのへんは指定できてような気がするので、なにかコントロールする設定方法があるのだろう、、、
今、このパッケージには src/main.rs しか含まれておらず、つまりこのパッケージはmy-projectという名前のバイナリクレートのみを持っているということです。 もしパッケージが src/main.rs と src/lib.rs を持っていたら、クレートは2つになります:どちらもパッケージと同じ名前を持つ、ライブラリクレートとバイナリクレートです。 ファイルを src/bin ディレクトリに置くことで、パッケージは複数のバイナリクレートを持つことができます。それぞれのファイルが別々のバイナリクレートになります。
src/
直下に rs ファイルを作ったら、そのファイル名と同名のクレートがいる、とみなされるらしい。
この節では、モジュールと、その他のモジュールシステムの要素 ――すなわち、要素に名前をつけるための パス 、パスをスコープに持ち込むuseキーワード、要素を公開するpubキーワード―― について学びます。 また、asキーワード、外部パッケージ、glob演算子についても話します。
例題のコードを見ると、名前空間とソースファイルの場所は 1:1 対応するものではないらしい。クレート(用語あってるか自信ない)の中で mod
をネストして宣言できる
// src/lib.rs
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn sear_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
以前、 src/main.rs と src/lib.rs はクレートルートと呼ばれていると言いました。 この名前のわけは、 モジュールツリー と呼ばれるクレートのモジュール構造の根っこ (ルート)にこれら2つのファイルの中身がcrateというモジュールを形成するからです。
lib.rs
も含めて、こいつらはモジュール階層のルートを形成するらしい(ファイル名は lib.rs
も固定値なのだろうか? main.rs
に関しては、慣例に従って cargo がこれを指すような仕様にしていると言及があった)。
モジュール階層のルートは crate
という名前で暗黙に構成されるらしい。そして、それは lib.rs
で宣言した名前に従うっぽい。
※ relly のソースを見てみるとそんな感じの雰囲気が出ている
// src/lib.rs of relly
mod bsearch;
pub mod btree;
pub mod buffer;
pub mod disk;
mod memcmpable;
pub mod query;
mod slotted;
pub mod table;
pub mod tuple;
クレートはコンパイル単位でモジュールは 1ファイルの中で(階層構造も含めて)複数定義可能であることを鑑みると、クレートはおおよそソースファイルの粒度に対応してる概念かな、という感じがする。
※ 複数のソースをまとめて1クレートとする、みたいなこともできるかもしれないので確信はないが、少なくとも1ファイルより細かい単位(コンパイル単位としてはファイルが最小単位のはず)でクレートを構成することはできない感じに読める。ここまでの理解では。
名前空間を指す概念として「パス」という用語が登場する。絶対/相対の2通りの指定方法があるのはイメージ通り
書き方に若干 Rust のクセがある。
さっきの例を引き合いに出すと、こうなる。
// src/lib.rs
mod front_of_house {
pub mod hosting { // モジュールを公開
pub fn add_to_waitlist() {} // 関数自身にも明示的に公開する修飾子を付ける必要がある
fn sear_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
// 参照方法の違い
pub fn eat_at_restaurant() {
// Absolute path
// 絶対パス
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
// 相対パス
front_of_house::hosting::add_to_waitlist();
}
絶対パスの場合は、モジュール階層のルートが暗黙に crate
になることを前提にした階層の表現になる。
crate::front_of_house::hosting::add_to_waitlist();
相対パスの場合は、まあ想像通りなので特に言うことなし。親ディレクトリにあたる ../
は、 super::<mod-name>
と super を付ける
強いて言えばファイルシステム上同じ階層にいる別ファイルでのモジュール宣言はカレントディレクトリのような扱いで参照できるのか?が知りたい。たぶん後から出てくるだろうし、いったんスルーする。
【疑問】なんでツリーの上位階層にあたる front_of_house
を公開しなくても動くんだろうか??
pub
キーワードは、ファイルシステムで言うところの enter 権限だと捉えていたが、どうも異なるらしい。どちらかというと pub
は read であり、 enter すること自体は pub
がなくとも問題ない、という感じで整理できるのかも
構造体や Enum も公開できる。ただし、構造体は フィールド単位で公開/非公開をコントロールできる。クラスの public/private メンバーに対応していそう
mod back_of_house { // 構造体の存在自体はここで公開できる(ただし、これだけだとフィールドは不可視のまま)
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String, // モジュールの外部には非公開のメンバ
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
※ impl
はメソッドの記法。 5.3 メソッド記法 を参照
Enum は公開すると配下のすべてのメンバーも公開状態になる。
mod back_of_house {
pub enum Appetizer { // 配下の Soup, Salad も公開状態になる
Soup,
Salad,
}
}
pub
の関わる場面として、use
キーワードとの絡みがある。長くなったので次に改める
use
は Python や TypeScript で言うところの from ~ import 構文に相当する。use を使えば、パスを現在のスコープに持ち込んでラクに参照できる。ファイルシステムで言うところのシンボリックリンクに似ている。
ローカルの名前と衝突するのが好ましくないのは Rust でも同様で、 use したい関数があればその親モジュールを use してコード内で名前空間を明示的に区別できるようにしておいた方が推奨、ということらしい
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
use キーワードでの指定が絶対/相対のどちらなのかはあんまり違いはなさそう。モジュールの構成がどう変わっていくか次第で使い分け、らしい。
use した名前に別名を与える機能は as
キーワードで利用することができる。このへんは python, typescript と同じく。
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
もともと別の場所で公開されていたモジュールを別のモジュールで use して、そこを基点にエクスポートすることも可能。 pub use
を使えば可能。
他言語との対応関係としては ... Private の概念が薄い Python だと、このへんは from import した時点で最初からそうなっている。TypeScript だと improt したモジュールを自身で export することが対応する
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting; // このモジュールの `hosting` という名前で公開する
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
モジュールの中身を宣言するためには
mod my_module {
// ...
}
とモジュール名に続けてブロックが続いた。
mod my_module;
とセミコロンを続けると意味合いが変わる。見た感じでは同一階層限定で機能する import (use) 文に見える...。 use とは何が違うのだろうか???
例題コードは以下
// lib.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();
}
// front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
}
use
と mod mod_name;
の違いについて
こんなことが書いてある。たぶん、太字の部分が大事な気がする。
定義は別のファイルにあるにもかかわらず、モジュールツリーは同じままであり、eat_at_restaurant内での関数呼び出しもなんの変更もなくうまく行きます。 このテクニックのおかげで、モジュールが大きくなってきた段階で新しいファイルへ動かす、ということができます。
src/lib.rs におけるpub use crate::front_of_house::hosting という文も変わっていないし、useはどのファイルがクレートの一部としてコンパイルされるかになんの影響も与えないということに注意してください。 modキーワードがモジュールを宣言したなら、Rustはそのモジュールに挿入するためのコードを求めて、モジュールと同じ名前のファイルの中を探すというわけです。
2つめのパラグラフはちょっと何言ってるのかわからない。たぶんクレートのコンパイル単位に関係してそうな気はするが。とりあえず実用上はスルーしても大きな支障はなさそうなのでスルーする。
トレイトについて、軽く書き方を調べる。 relly のソースを見ていたら
impl From<Option<PageId>> for PageId {
fn from(page_id: Option<PageId>) -> Self {
page_id.unwrap_or_default()
}
}
impl From<&[u8]> for PageId {
fn from(bytes: &[u8]) -> Self {
let arr = bytes.try_into().unwrap();
PageId(u64::from_ne_bytes(arr))
}
}
という記述があり、 From
の横にいる型はいったい何なのかがわからなかった。
例題のコードを見てみると、 impl ~for <struct>
のような書き方をすれば <struct>
のメソッドが実装できるらしい
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
基本の使い方はこれで良さそう。あとは、トレイト(インタフェース)を仮定する引数の仕様。これのサンプルは以下
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
仮引数の item: &impl Summary
がそれで、 &impl <trait>
と書けば、その関数、あるいはメソッドは <trait>
を実装したオブジェクトを何でも扱えるようになる。
impl
の宣言がある場所にジェネリックっぽい記述があるのは、ジェネリックにもトレイトを宣言できるかららしい
struct Pair<T> { x: T, y: T }
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T>
の T はちょっとよくわからない...。とりあえず関係はなさそうなのでメモるだけに留める
...
From トレイト という概念があるらしい。 relly のソースに書いてあるのはそれだった
#[derive(Debug)]
struct Point { x: f64, y: f64 }
impl From<f64> for Point {
fn from(input: f64) -> Self {
Point { x: input, y: input }
}
}
fn main() {
let p1 = Point::from(1.0);
let p2: Point = (1.0).into();
println!("{:?} {:?}", p1, p2);
}
型変換のメソッドを提供するもの、と思ってよさそう。
そういえば String::From
が実例だった。From を実装することでクラスメソッドを使って import ライクな使い勝手でインスタンス生成できるようになる、ということらしい。
で、特定の型から取り込むための From を生やしたら、今度はその「特定の型」の方からも変換が可能になる。
let p2: Point = (1.0).into();
と書いてあるのがそれで、ここでは左辺に型アノテーションがついていることがポイント。
Rust はコンパイル時に型を確定させることをよしとしているようなので、このアノテーションがあることでコンパイラはここでの into()
呼び出しが f64 から Point
型への変換であることを知り、From<f64>
トレイトとの対応関係がわかる。
トレイトとは若干関係が遠いけど、考え方の参考になった記事
変数を参照渡しにすること(?)を Boxing と言うらしい。 Rust ではこれを極力 やらないように 頑張っている、という特徴がある。メリットは
- ヒープアロケーションが楽になる
- 関数と、引数、戻り値の型がコンパイル時に確定する
前者は所有権システムにより密接に関連してそう。Boxing しないことで嬉しいのはたぶん後者。
特に印象に残ったのは次の文章
ボクシングをしない方針でいくと、 呼び出す関数の実装は、実行時でなくコンパイル時に完全に確定している必要がでてくる 。このことが書き方にも影響してくる。
コンパイラに色々(まだ内訳は理解できてない)情報を教えてあげよう、という考え方をする感じだろうか?
よく、こういう見た目のRustのコードがある。
let a: Vec<_> = iter.collect();
これは From トレイトの .into()
でも見た書き方。右辺に変換先の型がなくてもちゃんと変換できるのは、コンパイル時に型が確定する&している必要がある(だから左辺のアノテーションが要る)、というコンパイラの気持ちになるとなんとなく腑に落ちる