Open9

Rust に入門した その2

hassaku63hassaku63

https://zenn.dev/hassaku63/scraps/1e422ac2d1c745 の続き。

ライフタイムなどを組み合わせた構文規則がまだ読めないので、それを覚えて他人のコード(具体的には KOBA789/relly)を読めるようになることが目標

参考書籍:

https://www.amazon.co.jp/dp/B08PF27TRZ
https://www.amazon.co.jp/dp/B07QVQ7RDG

まずは後者の書籍を進めていく。

hassaku63hassaku63

逆ポーランド記法の式を解釈して演算する関数の実装課題。

トレイト境界クロージャ , where が出てきた。

fn main() {
  // 結果をスタックする mut stack<Vec>, スペース区切りで抽出したワード "token" を見ていくループ内
  // mtach arm (+)
  "+" => apply2(&mut stack, |x, y| x + y),
}

// トレイト境界
fn apply2<F>(stack: &mut Vec<f64>, fun: F) where F: Fn(f64, f64) -> f64,
{
  // ...
}

apply2 でトレイト境界と呼ばれるものを実装??している

fn 関数名<F>(.., 仮引数: F) where F: トレイト境界

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

型F はジェネリクスで、ここで実装しているトレイト境界は

Fn(f64, f64) -> f64

らしい。 where の次に記述したトレイト境界を守っていれば型 F はなんでもいい。

...とはいっても トレイト境界 という単語が入ってこないので解説を見てみた

トレイト境界の役割はジェネリクスの型パラメータとして受け付けられる型の範囲に境界を定めることです。ある型が境界を超えていなければ型パラメータの実引数として受け付け、超えているなら拒否します

κeen,河野 達也,小松 礼人. 実践Rust入門 [言語仕様から開発手法まで] (Japanese Edition) (Kindle の位置No.1836-1838). Kindle 版.

この說明を見ていると、「境界」はジェネリクスで指定した型 (F) に対する 制約 、という感じで理解しておくとよさそうな印象。実際に、C や Swift に存在する同等の機能のことを 型パラメータの制約 (type constraint) と呼ぶらしい。

※メソッドの定義に使う impl は「トレイト」という名前を冠しているが、あれとはまったく指すところが違っていそう??

java などの言語と違って Rust にクラス継承の考え方はない。しかし、「サブタイプ」という概念は存在するらしい。このへんの話に「ライフタイム」が絡んでいるらしい。ライフタイムも型の一部として扱わるため、関係しているみたい。今ライフタイムをやる気はないので、そういう話がある、ということだけ覚えておくことにする

hassaku63hassaku63

学び;

  • マルチスレッド対応
    • Rayon が超絶便利
    • rayon::join() で 2つのサブタスクを待ち受ける場合、タスクとして渡すクロージャの引数に注意が必要
      • (ベクタなどの)変数なら Send, クロージャなら Sync のトレイトを実装している必要がある
    • Sync
      • おそらくだが、スレッドに渡したタスクの実体はを元のプロセスに配置されたコードセグメントを共有している(スレッドごとにコードセグメントをコピーしたりしない)
      • そのため、同じメモリ配置をしているコードの上を複数のスレッドが通っても問題ないと「保証を宣言する」意味で Sync をつける必要がある(あくまで実際に担保するのはプログラマの仕事)
    • Send
      • 変数が複数スレッドから同時更新されると不都合、そのようなことは起きないと保証するトレイト
        • こちらも、実際に担保するのはプログラマの仕事...だと思われるが、自信はない。借用システムの可変参照の制約からすると、トレイト境界付けておけば勝手に Rust のランタイム側で保証してくれるんじゃないか?という気もしている...
  • mutable 参照の制約
    • 同じ実体に対して、mutable 参照は1つしか作れない
      • 所有権システムが、借用できないよと教えてくれる ... cannnot borrow
    • 今回のソート実装では、入力配列がここに引っかかった
      • スレッドに渡す前に、事前に配列を分割すればよい(しかし、単にスライスで分割しただけではNG)
      • Vecter の spilit_as_mut() を使うことで、1つのスライスを2つの可変参照に分割できる

Sync, Send のあたりは特に自信がない。プロセス内のメモリ展開を想像して書いてるので、違ってたらツッコミ、指摘が欲しい...。

hassaku63hassaku63

モチベ;

型システムをすることで、Rust の理解も深まる

型の分類

- Primitive Usre-defiend
Scalar bool, int, float, char -
Compound tuple, array, slice, string slice ... Vec<T>, String ...

スカラ型

null, none に相当する unit 型 (()) はスカラ型。すごく Lisp っぽい。

整数や浮動小数点数は、固定精度ならスカラ型。 (i32, usize, f32, f64 など)

参照 (referrence) と 生ポインタ (raw pointer) , 関数ポインタもスカラ型。

reference はメモリ安全で、 raw pointer はメモリ安全ではない。通常(?) は前者を使えばよく、後者は少々ユースケースが特殊。ポインタを他の言語との間で受け渡す/所有権システムの管理から外す、などの用途で利用される...らしい

※ raw pointer での参照外し・他のポインタ型への変換は、コンパイラが安全性を保証しない。そのため、これらを使う場合は unsafe ブロックで囲う必要がある。

※ "raw pointer" は書籍の便宜上の呼称であって正式には "pointer" 型が正しい

オーバーフローに関する挙動が面白い。例えば u8 型で、表現可能な範囲を超える加算を行う。

let n1 = std::u8::MAX;
let n2 = 1u8;

let n3 = n1 + n2;  // 256 になるが、u8 では表現不可能

println!("{}", n3);

デバッグモードでは panic になり、リリースモードではオーバーフローした結果 (0u8) が得られる。リリースモードでは、四則演算やシフト演算によって発生した桁あふれは検出できない

桁あふれの可能性がある場合に取れる対策がある。

  1. 検査付き演算 ... あふれなければ Some(value) を返し、そうでなければ None を返す。 checked_ で始まるメソッドが使える
  2. 飽和演算 ... 境界値が演算の上限/加減になる。最大値以上の結果は最大値、最小値未満の結果は最小値を返す。 saturating_ で始まるメソッド
  3. ラッピング演算 ... 桁あふれを無視する。値の範囲を周回するような振る舞いになる。 wrapping_ で始まる
  4. 桁あふれ演算 ... ラッピングと同じで桁あふれを無視。こちらは、桁あふれが発生したかどうかの bool を一緒に返す。overflowing_ で始めるメソッドが使える
hassaku63hassaku63

抜粋

複合型(プリミティブ)

配列 (array)

配列の型は要素の型と 要素数 で決まる。コンパイル時に決定し、長さは変更できない。(コンパイル時に index out of range が検出できるメリットでもある)

[u32; 10] // u32 型の要素を持つ長さ10の配列

可変長が欲しかったベクタ Vec<T> 型を使う必要がある。

そして、以下のような操作は配列自体が持つ機能ではなく、スライスが持っている。

  • index アクセス
  • len()
  • get()
  • iter()

配列に対してスライスのメソッドを呼び出そうとすると、その配列を暗黙にスライスへと型変換する 仕組みになっている(型強制)。

スライス

配列だけでなく、メモリ領域が連続するデータ構造ならどれでも対象にできる。

スライスへのアクセスは3通り。

  • 不変の参照 ... &[bool] など
  • 可変の参照 ... &mut [bool] など
  • Box ... Box<[bool]> など

スライスにも mutable, immutable の区別がある。immutable であれば、要素は変更できない。mutable で使えるメソッドには例えば以下のようなものがある

let mut x = [6, 4, 2, 8, 0, 9, 4, 3, 7, 5, 1, 7];

// sort, reverse
// 実際には &mut を付けなくても、暗黙の型強制が行われる
&mut x[2..6].sort();
&mut x[2..6].reverse();

// 2つの可変スライスへの分割。2つのスライスをそれぞれ可変で作ることはできないのでこれを使う
let (xa, xb) = &mut x.split_at_mut(5);

文字列スライス

str は文字列スライス。(文字列型ではない)

型の表記が Python や ES の感覚と全く違う。

文字列スライス型の表記は &str または &mut str

※疑問:じゃあ str は何の型なんだろうか・・・? -> ユーザー定義の複合型っぽい。後で登場する

リテラルの場合は " で囲って利用する。こちらの型は &'static str

'static はライフタイム指定子

str の値は utf-8 のエンコードを使用している。len() などのメソッドはバイト数を返すが、(日本語としての)文字数を返すことはできない。そして、標準ライブラリに日本語(=マルチバイト文字)の文字数を数える機能はないので注意。同様に「XX文字目」をカウントしたり、取得したりする機能も標準には入っていない。