Open39

Rust に入門した

hassaku63hassaku63

RDBMS 自作のネタをやりたいがために入門してみた。

開発環境: https://youtu.be/677kcyyPwJ4
入門: https://tourofrust.com/chapter_1_ja.html

執筆者の KOBA789 さんの記事 https://diary.hatenablog.jp/entry/2021/04/08/190000


他、参照できそうなリソース

hassaku63hassaku63
$ 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)
hassaku63hassaku63

なんか色々入ってることはわかった

$  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 はコンパイラ ... くらいの認識

hassaku63hassaku63
$ 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 らしい

hassaku63hassaku63

プロジェクトの始め方は cargo new とか cargo create あたりでいいのだろうか

hassaku63hassaku63

変数宣言は 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 が基本なのはよさそう

hassaku63hassaku63

プリミティブは若干自分が知ってる他の言語よりバリエーションがある

符号なし整数 ... 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
hassaku63hassaku63

const を使うと定数になる。このへんの構文は C 言語っぽい。

大文字のスネークケースを使い、型は明示する。

const SOME_CONST_VALUE: f32 = 3.14;
hassaku63hassaku63

配列は [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 ではそのへんはどうなるのだろうか。

hassaku63hassaku63

関数定義は python のそれに近い

fn add(x: i32, y: i32) -> i32 {
    return x + y;
}

タプルも返せる

fn swap(x: i32, y: i32) -> (i32, i32) {
    return (y, x);
}

戻り値の型が指定されていない 場合は unit と呼ばれる空のタプルを返す。(void とはどう違う?)

hassaku63hassaku63

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..50..=5の書き方はイテレータを生成する

while (true) に相当する構文として loop というのが使える。他にも使い方があるらしいが、後で說明するらしい

hassaku63hassaku63

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!");
        }
    }
}

結果は予想通り。

おそらく、タプルや配列などをマッチさせることもできるし、束縛時に展開することもできるはず。そのへんは後から出てくるものと期待する

hassaku63hassaku63

https://tourofrust.com/20_ja.html

セミコロンのあるなしは意味が異なるらしい。ブロックの最後にセミコロンを付けない場合、その最後の式がブロックの戻り値になる。

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 };
hassaku63hassaku63

struct は C 言語の通りっぽいので特にコメントしない。

メソッドの概念は、とりあえず static/instance method で呼び出し方が異なる、ということだけ覚える

hassaku63hassaku63

きりがなさそうなので、実践的なネタを押さえるために実際の例題を見てみる

当面やりたいのはこれ↓

https://youtu.be/GGd2P3htMhE

Result あたりがよくわかってない。あと、 unwarup も。

ざっと見た感じでは、Result 型をちゃんと処理したかったら match で Ok/Err を判別するし、楽するなら unwwap という選択肢もあり得るっぽい

参考になりそうなのはこのへん?

https://doc.rust-jp.rs/rust-by-example-ja/error/multiple_error_types/option_result.html

https://doc.rust-jp.rs/book-ja/ch09-00-error-handling.html

hassaku63hassaku63

&mut みたいな記述があって、これがよくわからない。

https://qiita.com/cactaceae/items/2c70a9947364c60ec100

  1. 変数を束縛(代入)すると元の変数はアクセスできなくなる
  2. 参照は幾つでも作れる
  3. 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 参照で渡す必要性が腑に落ちる

hassaku63hassaku63

例題に出てきたエクスクラメーションは一体何なのか??

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

https://blog-dry.com/entry/2020/11/02/000313

never 型の概念はいまひとつ有用性がわからない。TypeScript にも同様の型があり、たぶんモチベは似ているはず・・・。

それはそれとして、 println! のような関数名のエクスクラメーションに関しては単なる名前ではないかという感じもする.... → マクロの名前は必ずそうなる、という話らしい。 vec!println! に関してはこっちに該当する話で、上記の never 型とは別物らしい

https://qiita.com/yohhoy/items/e78dcc4d168f247d83ce#マクロmacro

hassaku63hassaku63

マクロについて調べてみたついでに、 try! というマクロが Result の取り回しに関連している雰囲気だったので調べた

https://doc.rust-lang.org/std/macro.try.html

// 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つ目の ? を使う方が簡潔に見える

hassaku63hassaku63

Result 関係の話と、早期リターンについて調べる

http://doc.rust-jp.rs/rust-by-example-ja/error/result/early_returns.html

早期リターンを知るには unwrap の理解も必要そうだったので、一緒に

http://doc.rust-jp.rs/rust-by-example-ja/error/option_unwrap.html

http://doc.rust-jp.rs/rust-by-example-ja/error/result/enter_question_mark.html

↑このドキュメント見てると、 ? は panic(回復不可能なエラー)の可能性をあえて無視して簡潔さを保つ、というモチベらしい。また、 try ではなく ? が今は推奨である とも書かれているのでここは覚えておきたい。そして、 ? と unwrap はかなり近い立ち位置のものらしいこともわかった

https://ja.stackoverflow.com/questions/1730/rustのunwrapは何をするものですか

unwrap() は、 Option<T> 型や Result<T, E> 型の値(つまり、何かしらの値を ラップ している値)から中身の値を取り出す関数です。たとえば Option<T> 型の値に対して unwrap() を呼ぶと、それが内包する T 型の値を返します。

unwrap() は失敗するかもしれないことに注意が必要です。 Option<T> 型や Result<T, E> 型などの値は、 T 型の値が入っていることもあれば入っていないこともあります。入っていない場合に unwrap() を呼ぶとプログラムは panic します。

なんとなくわかったような気はする

動画の題材で使っていた TcpListener 関係でやっていたことが、ちょっとわかってきた。

https://doc.rust-lang.org/std/net/struct.TcpListener.html

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 型の値を得ようとしていることがわかる。

hassaku63hassaku63

listener.incoming()

がよくわからないので整理する。

https://doc.rust-lang.org/std/net/struct.TcpListener.html#method.incoming

定義は以下だが、最後の型がよくわからない。あと、 &self というのも謎(こっちは python における self みたいなもの...??)

pub fn incoming(&self) -> Incoming<'_>

for 文で回していることや、ドキュメントの

Returns an iterator over the connections being received on this listener.

という記述から、戻り値の Incoming<'_> はどうやらイテレータらしいとわかる。

<'_> という表記は、anonymous lifetime と言うらしいがよくわからない。ライフタイムという概念はジェネリックと一緒に紹介されてたが、いったん理解は棚上げする

https://doc.rust-lang.org/edition-guide/rust-2018/ownership-and-lifetimes/the-anonymous-lifetime.html#_-the-anonymous-lifetime

hassaku63hassaku63

動画やドキュメントをあさりつつ、なんとか(警告は出るけど)意図したように 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();
    }
}
hassaku63hassaku63

TCP で簡易 HTTP サーバーを作ってみるにあたって、このへんも参考になりそう

https://note.com/marupeke296/n/ne2138bdc141e

https://cha-shu00.hatenablog.com/entry/2019/03/02/174532

以下は、読んでてほほー、となった記述。

bind関数の引数はToSocketAddrsというトレイトを継承しているオブジェクトですが、上のような「IPアドレス+ポート番号」も受け付けてくれます

TcpListener::bind("0.0.0.0:33333").expect("Error. failed to bind.");

expectはResult<T, E>から中身を取り出し、それがOkだった場合はその値を返し、Errだった場合引数の文字列とErrの値を表示してパニックを起こします。

その他、後から見つけたサンプルコード

https://gist.github.com/fortruce/828bcc3499eb291e7e17

hassaku63hassaku63

今後の方向性として、 note の記事を写経して echo サーバー作ってみるのもいいし、自作の Python 製 backlog api ラッパー を Rust 移植してみるのも良いかもしれない。

また、いくつか、実戦レベルの開発をやるにあたって押さえたいトピックはある

  • Testing
  • モジュール分割

この辺学べる題材があれば良さそう。

この scrap の意図である「入門してみた」に関しては、上記のどれかを追記するかも。いったんは本来やりたいことである RDBMS 自作の方に舵を切ることにする

hassaku63hassaku63

私達がこれまでに書いてきたプログラムは、一つのファイル内の一つのモジュール内にありました。 プロジェクトが大きくなるにつれて、これを複数のモジュールに、ついで複数のファイルに分割することで、プログラムを整理することができます。 パッケージは複数のバイナリクレートからなり、またライブラリクレートを1つもつこともできます。 パッケージが大きくなるにつれて、その一部を抜き出して分離したクレートにし、外部依存とするのもよいでしょう。 この章ではそれらのテクニックすべてを学びます。 相互に関係し合い、同時に成長するパッケージの集まりからなる巨大なプロジェクトには、 Cargoがワークスペースという機能を提供します。これは14章のCargoワークスペースで解説します。

ワークスペース、とかいう概念があるらしいことだけ覚えておく。

より重要そうなのは、以下

Rustには、どの詳細を公開するか、どの詳細を非公開にするか、どの名前がプログラムのそれぞれのスコープにあるか、といったコードのまとまりを保つためのたくさんの機能があります。 これらの機能は、まとめて「モジュールシステム」と呼ばれることがあり、以下のようなものが含まれます。

  • パッケージ: クレートをビルドし、テストし、共有することができるCargoの機能
  • クレート: ライブラリか実行可能ファイルを生成する、木構造をしたモジュール群
  • モジュール と use: これを使うことで、パスの構成、スコープ、公開するか否かを決定できます
  • パス: 要素(例えば構造体や関数やモジュール)に名前をつける方法

クレートがビルド単位に焦点を当てた概念、パッケージはプロジェクト全体ぽい、モジュールやパスがパッケージ(あるいはクレート?)を構成する部分概念なのかな?という印象

hassaku63hassaku63

https://doc.rust-jp.rs/book-ja/ch07-01-packages-and-crates.html

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 ファイルを作ったら、そのファイル名と同名のクレートがいる、とみなされるらしい。

hassaku63hassaku63

https://doc.rust-jp.rs/book-ja/ch07-02-defining-modules-to-control-scope-and-privacy.html

この節では、モジュールと、その他のモジュールシステムの要素 ――すなわち、要素に名前をつけるための パス 、パスをスコープに持ち込む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ファイルより細かい単位(コンパイル単位としてはファイルが最小単位のはず)でクレートを構成することはできない感じに読める。ここまでの理解では。

hassaku63hassaku63

https://doc.rust-jp.rs/book-ja/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html

名前空間を指す概念として「パス」という用語が登場する。絶対/相対の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 権限だと捉えていたが、どうも異なるらしい。どちらかというと pubread であり、 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 キーワードとの絡みがある。長くなったので次に改める

hassaku63hassaku63

https://doc.rust-jp.rs/book-ja/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html

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();
}
hassaku63hassaku63

https://doc.rust-jp.rs/book-ja/ch07-05-separating-modules-into-different-files.html

モジュールを複数のファイルに分割する

モジュールの中身を宣言するためには

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() {}
}

usemod mod_name; の違いについて

こんなことが書いてある。たぶん、太字の部分が大事な気がする。

定義は別のファイルにあるにもかかわらず、モジュールツリーは同じままであり、eat_at_restaurant内での関数呼び出しもなんの変更もなくうまく行きます。 このテクニックのおかげで、モジュールが大きくなってきた段階で新しいファイルへ動かす、ということができます。

src/lib.rs におけるpub use crate::front_of_house::hosting という文も変わっていないし、useはどのファイルがクレートの一部としてコンパイルされるかになんの影響も与えないということに注意してください。 modキーワードがモジュールを宣言したなら、Rustはそのモジュールに挿入するためのコードを求めて、モジュールと同じ名前のファイルの中を探すというわけです。

2つめのパラグラフはちょっと何言ってるのかわからない。たぶんクレートのコンパイル単位に関係してそうな気はするが。とりあえず実用上はスルーしても大きな支障はなさそうなのでスルーする。

hassaku63hassaku63

https://doc.rust-jp.rs/book-ja/ch10-02-traits.html

トレイトについて、軽く書き方を調べる。 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> を実装したオブジェクトを何でも扱えるようになる。


https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/497a21

https://doc.rust-jp.rs/book-ja/ch10-02-traits.html#トレイト境界を使用してメソッド実装を条件分けする

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> トレイトとの対応関係がわかる。

hassaku63hassaku63

トレイトとは若干関係が遠いけど、考え方の参考になった記事

https://qiita.com/hadashiA/items/d0c34a4ba74564337d2f

変数を参照渡しにすること(?)を Boxing と言うらしい。 Rust ではこれを極力 やらないように 頑張っている、という特徴がある。メリットは

  • ヒープアロケーションが楽になる
  • 関数と、引数、戻り値の型がコンパイル時に確定する

前者は所有権システムにより密接に関連してそう。Boxing しないことで嬉しいのはたぶん後者。

特に印象に残ったのは次の文章

ボクシングをしない方針でいくと、 呼び出す関数の実装は、実行時でなくコンパイル時に完全に確定している必要がでてくる 。このことが書き方にも影響してくる。

コンパイラに色々(まだ内訳は理解できてない)情報を教えてあげよう、という考え方をする感じだろうか?

よく、こういう見た目のRustのコードがある。

let a: Vec<_> = iter.collect();

これは From トレイトの .into() でも見た書き方。右辺に変換先の型がなくてもちゃんと変換できるのは、コンパイル時に型が確定する&している必要がある(だから左辺のアノテーションが要る)、というコンパイラの気持ちになるとなんとなく腑に落ちる