“Trait”がPHPにもある概念だということを遅まきながら思い出した.使ったことないけど…….

The book 第7章 “Managing Growing Projects with Packages, Crates, and Modules”をよむ

機能の分割の一般的な効用について.Package, crate, moduleの区別を意識する必要がある.

7.1 Packages and Crates

cargo new my-projectコマンドで作られるのはmy-projectパッケージ. パッケージはCargo.tomlを持ち,クレートがどのようにビルドされるかの指示書になっている. src/main.rsはパッケージと同名のmy-projectバイナリクレートのクレートルート. src/lib.rsというファイルを持つこともできて,こちらはmy-projectライブラリクレートのクレートルート. クレートは名前空間を定める.

7.2 Defining Modules to Control Scope and Privacy

モジュールはちょうどディレクトリのように関数,列挙型,定数……etc.に名前を付けてまとめる. これはコーディング時と利用時にどこに何の機能があるか探し当てるのに役に立つ.

7.3 Paths for Referring to an Item in the Module Tree

あるモジュールにある機能を利用するには,module tree上のパスを指定する.絶対パス(crate::から始める)と相対パスのどちらも使える.

モジュールはprivacy boundaryを定める.Rustでは,デフォルトのprivacyのレベルはprivateにあたる.外部からの利用にはpubキーワードを付ける必要がある.

superキーワードは親モジュールへの相対パスの起点として使える.

構造体の関数やフィールドを外部から利用するには,たとえ構造体自身がpubであっても明示的にpubキーワードを付ける必要がある.

列挙型のvariatnは,その列挙型自身がpublicなら自動的にpublicになるためpubキーワードを付けない.

7.4 Bringing Paths into Scope with the use Keyword

use文でシンボリックリンクを貼るように,パスによる機能の呼び出しを短縮できる.

use crate::parent_module_name::child_module_nameを書くとchild_module_name::function_name()で関数が使える.慣習として,外部の関数を使うときはuse文はその関数の親モジュールまでに留める.関数ごとuseしてもよいが,それの由来が分からなくなってしまうから.構造体/列挙型を持ち込むときはその構造体/列挙型名までuseする.文法的には禁じられていないがrustaceanたちはこれに慣れている.

Resultは同名の構造体がstd::fmtstd::ioモジュールに含まれるため,同時に単にResultだけで指定することはできない.この場合,asを使ってエイリアスを定義して利用することができる.

Cargo.tomlの[dependencies]へパッケージをバージョンとともに書き込み,useすることで外部パッケージが使える.

use文はカーリーブラケットでネストできる.

use ~~::*で全てのpublicな要素をuseできる.

と書いていて気付いたが,”import”という語が出てこないな.”Export”もre-exportingの形でしか登場しない.

7.5 Separating Modules into Different Files

src/lib.rsの先頭にmod module_name;を宣言すると,src/module_name.rssrc/lib.rs内に書き込んだ場合と同じようにモジュールとして利用できる.入れ子のモジュールはディレクトリに対応する.

この章で説明されたことは概念レベルでは他の言語にもある話だから実際書いていけば分かるはず……. 外部クレートの利用がCargo.tomlに書くだけで済む?のは特徴的か.あとデフォルトでprivateであること.Javaのpackage privateとも違う.

The book 第8章 “Common Collections”をよむ

複数の値をもつデータ構造をcollectionと呼ぶ. 配列やタプルはコンパイル時からサイズ固定でスタック上に保持されるが,collectionはヒープ上にあってランタイム時に値の追加や削除が行われ,サイズが変わる.

std::collections - Rust

標準ライブラリのコレクションたち.前も見たがQueueStackそのものはない.

Vecは末尾への値の追加/削除が高速なので,Stackとしても使える.VecDequeは名前の通りdeque.

LinkedListの説明:

You want a Vec or VecDeque of unknown size, and can’t tolerate amortization.

“Amortization”が分からなかった. algorithm - Constant Amortized Time - Stack Overflow

You are absolutely certain you really, truly, want a doubly linked list.

なんだこの念押しは…….

優先度付きキューはBinaryHeapを使う.

このページちょいちょい見ることになりそう.

8.1 Storing Lists of Values with Vectors

Vec型.ジェネリクスで定義されるため,new()で初期化する場合はVec::<i32>のような型注釈が必要.

値付きで初期化する場合,vec![]マクロが使える.

値の末尾への挿入は.push(value)

スコープを抜けた瞬間にベクターは要素の値ごとクリーンナップされる.

値の参照は&+[]オペレータか.get()メソッドによる.後者はOption<&T>型を返す.前者は存在しない位置を指したときパニックするが後者はエラーハンドリングが効く.

同じスコープ内に不変参照と可変参照を同時に持つことはできないというルールは,ベクターの要素とベクターそのものにも適用される.ベクターの要素への不変参照を持っている間は,ベクターへの要素の挿入はできない.これは,要求するメモリサイズの増大に従ってベクターのデータが丸ごとコピーされて元の要素への参照が宙吊りになってしまうことが起こりうるため.

不変参照に対するイテレーションはfor element in &vectorで行える.値の変更を行う場合は,for element in &mut vector*elementによるでリファレンスを行う.

異なる型のデータを同じベクターに載せたいときは,列挙型を定義すればよい.TypeScriptの合併型みたいなことは列挙型で実現できるということか.Option<T>はnullableを作る方法にもなっている.TSだとランタイムの型による分岐を書くためにはかなり制限が付くけれど,Rustだとmatch式でだいたいそれが実現できる.

8.2 Storing UTF-8 Encoded Text with Strings

String型は,テキストとして解釈するためのメソッドを備えたbyteのcollectionとみなすことができる. ただし,データ上のインデックスと人にとってのインデックスが異なるため,ベクターと同じではない. エラーの扱いを厳格なものにするRustの設計思想のため扱いが難しい.

Rustに予め備わっている唯一の文字列型はstring sliceであるstr.単に”string”というときは,String&strのどちらも指す. 両方ともUTF-8でエンコードされている.

空文字はString::new()で初期化できる.これそのものやlenメソッドで長さを算出してprintしてもエラーにならない.

Displayトレイトを備える型はto_stringメソッドでString型へ変換できる.str型にもこれは備わっている.

文字列リテラル(str)からStringを作るもう一つの方法はString::from.既に見た.

文字列末尾への文字列の追加はpush_strメソッドで行える.pushメソッドは単一の文字(char型)を追加する.

+演算子によっても文字列を結合できるが,実体はaddメソッドでシグネチャが(self, s: &str)のようになっているため,第1引数の所有権はムーブする.第2引数には参照を取る.所有権のムーブが起こるのは一見奇妙だが,コピーをむやみやたらと生成しない点で節約的になっている.

&strを取るべきところに&Stringがある場合,コンパイラは&String型を&strcoerceする.強制型変換?

format!マクロはprintln!と同様に変数からフォーマットされた文字列をString型の文字列として返す.所有権のムーブは起こらない.

ところで,

    let s0 = String::from("tic");
    let s1 = String::from("tac");
    let s2 = String::from("toe");
    let s = format!("{}-{}-{}", s0, s1, s2);

を書くためにInsert NumbersというVSCode拡張を入れた.便利! VSCodeのマルチカーソル練習帳 - Qiita

String型の変数を構成する成分への[]オペレータによるアクセスはコンパイルしない.

error[E0277]: the type `String` cannot be indexed by `{integer}`
  --> src\main.rs:28:13
   |
28 |     let c = s[0];
   |             ^^^^ `String` cannot be indexed by `{integer}`
   |
   = help: the trait `Index<{integer}>` is not implemented for `String`

Index<{integer}>トレイトをimplementしていないことによる.

StringVec<u8>のラッパーである.

    let en = String::from("hello");
    let ru = String::from("Здравствуйте");
    let jp = String::from("こんにちは");
    println!("{} {}", en, en.len());
    println!("{} {}", ru, ru.len());
    println!("{} {}", jp, jp.len());
hello 5
Здравствуйте 24
こんにちは 15

なるほど.

pub fn len(&self) -> usize Returns the length of this String, in bytes, not [char]s or graphemes. In other words, it may not be what a human considers the length of the string.

たとえラテン文字でも文字列の要素へのインデックスによる参照はコンパイルしない.

ところで国名のtwo-letter abbreveationがISO 3166で決まっていることを知った. ISO 3166-1 alpha-2 - Wikipedia

Bytes, scalar values, grapheme clusters. 書記素クラスタ.「書記素」←単語登録した.『文字コード技術入門』にも載っているな.

デーヴァナガリーの“नमस्ते”はcharの配列として['न', 'म', 'स', '्', 'त', 'े']に分解するが,4, 6文字目はdiacritics(ダイアクリティカルマーク.発音区別記号)であって単独では意味をなさない.charは”letter”ではない.Grapheme cluseterとしての分解は["न", "म", "स्", "ते"]で,「ヒンディー語で4字」といったときの長さになる.

[]による取得を実装しないもう一つの理由は,[]は定数時間O(1)であることを要求するルールにある.マルチバイト文字を含む場合,n文字目の取得には先頭から数え上げるしかない(O(n))から,意図的に実装していないともいえる.なるほど.

文字列スライスの取得はコンパイルするがランタイムでパニックする.インデックスが単独の文字の途中にくる場合.

    println!("[..6] {}", &jp[..6]);
    println!("[..7] {}", &jp[..7]);
[..6] こん
thread 'main' panicked at 'byte index 7 is not a char boundary; it is inside 'に' (bytes 6..9) of ` 
こんにちは`', src\main.rs:36:27

前から文字数を数えて使うことを想定しているのだろうか.

.chars.bytesメソッドのそれぞれが,charとbyteのイテレータを生成する.Grapheme clusterへの分解は遥かに複雑であるためstdではサポートしておらず,適当なクレートを探してくる必要がある.

“Unicode scalar value”というときの”scalar”ってなんだ?

Emojiを分解する.

    let emoji = String::from("👩🏻‍💻");
    for c in emoji.chars(){
        println!("{}", c);
    }

    for b in emoji.bytes(){
        println!("{}", b);
    }

出力:

👩
🏻

💻
240
159
145
169
240
159
143
187
226
128
141
240
159
146
187

4つのcharからなっている.肌色で女性,明るい肌色,ゼロ幅joiner,ラップトップ.

The Woman Technologist: Light Skin Tone emoji is a ZWJ sequence combining 👩 Woman, 🏻 Light Skin Tone, ‍ Zero Width Joiner and 💻 Laptop. 👩🏻‍💻 Female Technologist: Light Skin Tone Emoji

胸に刻もう.

To summarize, strings are complicated.

8.3 Storing Keys with Associated Values in Hash Maps

HashMap<K, V>型.任意の型をキーに持つことができるコレクション.Preludeに入っていないため,use std::collections::HashMapでスコープに入れる必要がある.初期化するためのビルトインのマクロなどもない.

これ難しいな…….

    let teams = vec![String::from("Blue"), String::from("Yellow")];
    let initial_scores = vec![10, 50];
    let mut scores: HashMap<_, _> = teams.into_iter().zip(initial_scores.into_iter()).collect();

zipメソッドの説明:

zip() returns a new iterator that will iterate over two other iterators, returning a tuple where the first element comes from the first iterator, and the second element comes from the second iterator.

HashMapもやはり値の代入で値のムーブが発生する.参照を代入したときは,元の値のライフタイム内に制限される.

for文で回した場合,実行順序はランダムに変わる.

    for (key, value) in &scores{
        println!("{} {}", key, value);
    }

1回目:

Yellow 50
Red 70
Green 300
Blue 10

2回目:

Blue 10
Red 70
Green 300
Yellow 50

HashMapの更新は,キーが既に存在するかどうか,存在した場合上書きするか何もしないかなど,場合によって色々な振る舞いが必要になる.

insertは既に存在する場合値を上書きする.

キーが既に存在する場合に上書きしたくなければ,entryor_insertメソッドを繋げることでそれができる.C++みたいにcountによる分岐を書かなくてよい.クールだ.

キーが既にある場合に元の値に基づいて上書きしたければ,or_insertがmutable referenceを返すのでそれをdereferenceして使う.

HashMapのハッシュ生成にはSipHashというハッシュ関数が用いられている.最速ではないがセキュリティー上の利点がある.

日本語Wikipedia記事がないがヘブライ語がある.

SipHash - Wikipedia

SipHash – ויקיפדיה

章末の演習問題をやってみよう.