Closed16

中途半端Rustからの再学習

ゆでゆで

現状

コンパイラに従いながらある程度コードを書くことができ、所有権や可変変数、借用チェックなどがわかる。
Result 型や Option 型が使え、 anyhow を用いたエラーハンドリングはわかるが、最適なエラーハンドリングには悩むところがある。
また、structimpl が使えるが、 trait や ライフタイムなどはイマイチわかってない。
Box dyn もよくわかってない。
ようするに中途半端であやふや。

他の言語は、lua, js/ts が歴の長い使用言語で、 C/C++, C#, Python は時折触る程度。

ゆでゆで

トレイト(1)

interface と似たものであることはわかっているが、具体的にどう違うのか、何ができるのか、という段階。

トレイトは

  • データ型を分類するための仕組み
  • 型に対して共通の振る舞いを定義できる
  • ジェネリック型にトレイト境界として指定し、インスタンスを制約できる

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

トレイトは下記のようにして定義。

trait Entry {
    fn get_name(&self) -> &'static str {
        "Undefined"
    }
    fn get_parent(&self) -> &'static Self;
    fn new(name: &str) -> Self;
}

get_name のように、デフォルト実装もできる。
多分実装のときに困るが、コンパイルエラーは出ないので一旦このままで。

ゆでゆで

ライフタイム(1)

スコープを抜けたらオブジェクトが死ぬことはわかっているが、 'static とか 'a とか強要されるのなんですか? 状態。

複数の型の可能性があるときには、型を注釈しなければなりません。
同様に、参照のライフタイムがいくつか異なる方法で関係することがある場合には注釈しなければなりません。

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/fbd089

関数や構造体の実装において時折 <'a> のようなものが強要されるのはこれか。

-

ライフタイムの主な目的は、ダングリング参照を回避することです。
ダングリング参照によりプログラムは、 参照するつもりだったデータ以外のデータを参照してしまいます。

https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html

ダングリング参照という言葉は初めて聞いたが事象は知ってた。free() したオブジェクト(宙ぶらりんになってしまったポインタ)が残ってるやつ。

無効なメモリ領域を指すポインタはダングリングポインタ(dangling pointer)と呼ばれる。
とりわけ、本来有効だったメモリ領域が解放処理などによって無効化されたにもかかわらず、そのメモリ領域を参照し続けているポインタのことを、ダングリングポインタと呼ぶ。
なお、ダングリングポインタは「ぶら下がりポインタ」と訳されることもある。

https://marycore.jp/coding/dangling-pointer/

ダングル (dangle) は「ぶら下がる」という意味だそう。

下記で借用チェックの説明にて、'a'b を用いている。

https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#借用精査機

暗黙的にライフタイムをコンパイラが推測しているときはわざわざ宣言する必要はなかったが、ライフタイムの注釈がつくならばこうなるのだと改めて理解。

下記の関数はエラーを吐く。

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#関数のジェネリックなライフタイム

fn longest(x: &str, y: &str) -> &str は、型のことだけを考えれば正しいが、ライフタイムがわからないから。
xy のどっちのライフタイムが返ってくるんですか? って話。

下記の関数はコンパイルできる。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#関数シグニチャにおけるライフタイム注釈

一見すると、引数として与えられる xy もライフタイムが同じであることが条件であるようだが、実はどちらかライフタイムの短い方に依存する。

ゆでゆで

ライフタイム(2)

構造体を持つ構造体を以下のように定義すると、コンパイルが通る。

struct Parent<'a> {  
   ref_chile: &'a Child  
}  
  
struct Child(usize);

Parent は、フィールドにある Child の不変の参照よりもライフタイムが短い、ということをコンパイラに伝えている。

https://blog-mk2.d-yama7.com/2020/12/20201230_rust_lifetime/

親より先に死ぬ子供があってたまるか、という状態(?)。

impl<'a> Parent<'a> {  
    pub fn get_child_age(&self) -> usize {  
        self.ref_child_a.0
    }

    pub fn compare_child_age(&'a self, another_child: &'a Child) -> &'a Child {  
         if self.ref_child_a.0 > another_child.0 {  
            self.ref_child_a  
        } else {
            another_child  
        }  
    }
}

構造体にライフタイム注釈があるならば、メソッドでライフタイム注釈を使わなくとも、 impl は必ずライフタイム注釈を付ける必要がある。

また、 compare_child_age に関するライフタイム注釈を全て省略すると、下記のような推論が行われる。

fn compare_child_age(&'a self, another_child: &'b Child) -> &'a Child

これは、ライフタイム省略規則というものが係るからである。

  1. 参照型の引数はそれぞれ独自のライフタイムをもつ(=それぞれ異なるライフタイム注釈をつけることがでこきる)
  2. 引数の中で参照型が1つだけなら、その引数のライフタイムと戻り値(参照)のライフタイムと同一とみなす
  3. 第一引数が &self または &mut self ならば、戻り値(参照)のライフタイムは self と同一とみなす

1 より、ライフタイム注釈は各引数に対して個別に付けられ
3 より、戻り値のライフタイム注釈は 'a と一緒になる

-

引数のライフタイムは 入力ライフタイム と呼ばれ、返り値のライフタイムは 出力ライフタイム と呼ばれる。

議論する必要のある1種の特殊なライフタイムが、 'static であり、これは、この参照がプログラムの全期間生存できる事を意味します。
この文字列のテキストは、プログラムのバイナリに直接格納され、常に利用可能です。
故に、全文字列リテラルのライフタイムは、 'static なのです。

エラーメッセージで、 'static を使用するようことがありますが、その参照が本当にプログラムの全期間生きるかどうか考えてください。
ほとんどの場合、問題は、ダングリング参照を生成しようとしているか、利用可能なライフタイムの不一致が原因です。
そのような場合、解決策はその問題を修正することであり、'static ライフタイムを指定することではありません。

https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#静的ライフタイム

トレイトの際に下記のような実装をしたが、上記の文章を読むと、 'static が適切ではないことがわかる。

trait Entry {
    fn get_name(&self) -> &'static str {
        "Undefined"
    }
    fn get_parent(&self) -> &'static Self;
    fn new(name: &str) -> Self;
}

上記の学習に基づくと、下記のような実装でいいことになりそう。

trait Entry<'a> {
    fn get_name(&self) -> &str {
        "Undefined"
    }
    fn get_parent(&self) -> &Self;
    fn new(name: &str) -> Self;
}

省略規則を適用するとこんな感じか

trait Entry<'a> {
    fn get_name(&'a self) -> &'a str {
        "Undefined"
    }
    fn get_parent(&'a self) -> &'a Self;
    fn new(name: &'a str) -> Self;
}

Entry トレイト内のライフタイムは全て <'a> と同等か、それより長く生存すると想定するならば、これであっていることになる。

ゆでゆで

トレイト(2)

トレイトは継承を行うことができる。

pub trait Geometry {
    ...
}

pub trait Drawable: Geometry {
    ...
}

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/497a21#📌-トレイト継承

ある型から別の型に変換するとき、便利な From トレイトというのがあります。
from メソッドを対応した型ごとに実装することで、その型から into メソッドで変換できます。

#[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);
}

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/497a21#📌-from-トレイト

型を指定してあげなくても先の方を予測して勝手に変換してくれる仕組み、便利だなぁと思いつつ実装方法を知らなかった。

ゆでゆで

RAII(1)

リソースの確保をオブジェクトの初期化時に行い、リソースの開放をオブジェクトの破棄と同時に行う手法。

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/063df5#📌-raiiとは

https://doc.rust-jp.rs/rust-by-example-ja/scope/raii.html

スコープから抜けたら所有権が破棄されるのは既知のとおりだが、C/C++のような言語だと、リソースの開放を自動的にやってくれる訳では無い。そうしたいなら std::unique_ptr を使う必要がある。
Rust では明示的に破棄をする必要はないが、それを明示的にするにはどうするべきか、が記述されている。

ヒープメモリを確保するには、 Box<T>::new を使用する。

let a = Box::new(10);        // type inference
let a = Box::<i32>::new(20); // explicit type
let a = 30;                  // immutable object
let b = Box::new(a);         // move object from stack memory to heap memory
let c = *b;                  // dereference

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/063df5#📌-box型

Box を使う例として二分木がある。

struct BinaryTree<T: std::cmp::PartialOrd + std::fmt::Display> {
    val: T,
    left: Option<Box<BinaryTree<T>>>,
    right: Option<Box<BinaryTree<T>>>,
}

impl<T: std::cmp::PartialOrd  + std::fmt::Display> BinaryTree<T> {
    fn new(val: T) -> Self {
        Self {
            val,
            left: None,
            right: None,
        }
    }

    fn insert(&mut self, val: T) {
        if val < self.val {        
            println!("insert value {val} to left");
            self.insert_left(val);
        } else {
            println!("insert value {val} to right");
            self.insert_right(val);
        }
    }

    fn insert_left(&mut self, val: T) {
        match &mut self.left {
            Some(left) => {
                // left..insert(val);
                left.insert(val);
            }
            None => {
                self.left = Box::new(BinaryTree::new(val)).into();
            }
        }
    }

    fn insert_right(&mut self, val: T) {
        match &mut self.right {
            Some(right) => {
                right.insert(val);
            }
            None => {
                self.right = Box::new(BinaryTree::new(val)).into();
            }
        }
    }
}

動作ほぼ未確認だがだいたいこんな感じ。

ゆでゆで

RAII(2)

同じオブジェクトを複数から束縛することを可能にするのが参照カウンタ Rc<T>
Rc型はオブジェクトをヒープメモリに置くので、Box の代わりに使うことができる。

use std::rc::Rc;

let a = Rc::new(10);
let b = Rc::clone(&a);

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/063df5#📌-rc

この入門記事では「ポインタ」という言葉を避けているが、この Rc<T> は、 C++ でいう std::shared_ptr であると理解した。

複数の変数に同じオブジェクトへの所有権をもたせ、全てが破棄された時点でメモリの開放を行う。

また、 Arc<T> はスレッドセーフな参照カウンタである。

use std::rc::Rc;

fn main() {
    let foo = Rc::new(50);

    // reference count = 1
    println!("reference count :{}",Rc::strong_count(&foo));

    let bar = Rc::clone(&foo);

    // reference count = 2
    println!("reference count :{}",Rc::strong_count(&bar));

    // &foo == &bar
    println!("foo = {:p}", foo);
    println!("bar = {:p}", bar);
}

https://dev.classmethod.jp/articles/rust-smart-pointer/

ただ、Rc で値を取ってきても可変ではないので、書き換えることはできない。

ゆでゆで

内部可変性(1)

Cell<T> を用いると、mut 宣言しなくとも値を変更することができる。

use std::cell::Cell;

fn main() {
    let a = Cell::new(10); // immutable object with interior mutability
    dbg!(a.get()); // a.get() = 10
    a.set(20);
    dbg!(a.get()); // a.get() = 20

    let b = a.replace(10);
    dbg!(a.get()); // a.get() = 10
    dbg!(b);       // b = 20
    
    let c = a.into_inner(); // turn Cell<T> into T
    dbg!(c);       // c = 10
    dbg!(a);       // borrow check - Error
}

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/5df75e#📌-内部可変性

get/set はそのままなので置いといて、
replace は内部の値を変更しつつ、以前の値をポップアウトする。
into_inner はポップアウトする。

この Cell<T> と、先の Rc<T> を組み合わせて、可変な std::shared_ptr を作り出す。

fn main() {
    let a = Rc::new(Cell::new(10));
    a.set(20);
    dbg!(a.get()); // a.get() = 20

    let b = Rc::clone(&a);
    b.set(30);
    dbg!(a.get()); // a.get() = 30
}

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/5df75e#📌-内部可変性

ゆでゆで

関数、クロージャ(1)

クロージャはそれぞれ独自の型を持っています。
クロージャは Fn トレイト、 FnMut トレイト、 FnOnce トレイトのどれかのインスタンスです。
それぞれ &self&mut selfself を内部的に引数として受け取っているかどうかの違いがあります。
また、Fn トレイトは FnMut トレイトを、 FnMut トレイトは FnOnce トレイトを継承しています。

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/64c6f3#📌-クロージャとは

fn は関数定義で使いますが、 fn は型でもあります。
そして、 fn のことを 関数ポインタ と言います。
fnFn トレイトのインスタンスなので、 FnMut トレイト、 FnOnce トレイトのインスタンスでもあります。

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/64c6f3#📌-fn-と-fn

急にようわからんこと&知らんことがいっぱい出てきた。ので詳細に書いている記事を漁った。

fn main() {
    let closures = [3, 7, 1, 5, 8, 9, 2].iter().map(|&i| {
        move |j| i + j
    }).collect::<Vec<_>>();
    println!("{}", closures[3](14));
}

というコードでは、「3を足す関数」「7を足す関数」「1を足す関数」「5を足す関数」…… のようにたくさんの「関数」を動的に生成していますが、こういうのはクロージャでないとできません。

https://qnighy.hatenablog.com/entry/2018/02/11/220000

move ? なんだこれ
と思ったので更に調べる。

// moveするクロージャ
let mut num = 0;
// 本当は所有権がムーブするが、数値はCopyな型なのでコピーされる
let mut ref_cls = move || {
    num += 1;
    num
};
assert_eq!(ref_cls(), 1);
// numはクロージャとは無関係になったので影響を受けない
assert_eq!(num, 0);

// moveするクロージャ
let s = "String".to_string();
let ref_cls = move || {
    println!("{}", s);
};
// `s`は既にムーブしているので使うとエラー
// println!("{}", s);

https://teratail.com/questions/377403

本当に所有権が移るようになるっぽい。

fn main() {
    let x = vec![1, 2, 3];

    let equal_to_x = move |z| z == x;

    // ここでは、xを使用できません: {:?}
    println!("can't use x here: {:?}", x);

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

    assert!(equal_to_x(y));
}

https://doc.rust-jp.rs/book-ja/ch13-01-closures.html

読むたび何か大事なことをすっぽかしているような気がするので、公式ドキュメントを読み返すことに。

ゆでゆで

関数、クロージャ(2)

実行に時間がかかる処理を持つ関数を呼び出すのを、不要なら呼び出さず、なおかつ2回以上呼び出す場合は前回の値を流用してね、というような処理を作るチュートリアル。
その中でキャッシュをもたせる構造体を作成したが、限界を悟ってクロージャを用いようねっていう流れになっていた。

https://doc.rust-jp.rs/book-ja/ch13-01-closures.html

その中で Fn FnMut FnOnce の説明が入っていた。

直感的には、
FnOncemove || {} に実装されるトレイトで、クロージャ内でキャプチャした変数に所有権を渡す。所有権は二回以上奪うことができないので、Once という名前がついている。
FnMut はクロージャ内で可変で値を借用する。
Fn は不変で借用する。

という感じ。

move は並列処理で多く出てくるそう。

で関数ポインタ、関数定義型って?

関数を参照するポインタが関数ポインタなんだろうというところはわかるが、「fn が関数ポインタ」という文言がわからない。

関数は、型 fn に型強制されます。
fn 型は、関数ポインタと呼ばれます。

https://doc.rust-jp.rs/book-ja/ch19-05-advanced-functions-and-closures.html#関数ポインタ

うーん

ゆでゆで

関数、クロージャ(3)

fn type_of<T>(_: T) -> String {
    let a = std::any::type_name::<T>();
    return a.to_string();
}

fn double(value: i32) -> i32 {
    value * 2
}

let f1: fn(i32) -> i32 = double;
let f2 = double;

println!("function pointer: {}", type_of(f1)); // function pointer: fn(i32) -> i32
println!("function pointer: {}", type_of(f2)); // function pointer: calc::main::double

https://yukimemo.hatenadiary.jp/entry/2021/01/24/152948

型を示してポインタとして受け取るならばそれは関数ポインタとなり、そうでない場合は関数定義型になる。

https://doc.rust-lang.org/reference/types/function-item.html

イマイチ腑に落ちないのでとりあえずさようなら。

下記のコードはコンパイルが通らない。返すのが型ではないから。

fn returns_closure() -> Fn(i32) -> i32 {
    |x| x + 1
}

通すだけならば以下の通り。

fn returns_closure() -> fn(i32) -> i32 {
    |x| x + 1
}

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

fn returns_closure<'a>() -> &'a (dyn Fn(i32) -> i32) {
    &|x| x + 1
}

fn returns_closure() -> Box<dyn Fn(i32) -> i32>{
    Box::new(|x| x + 1)
}

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/64c6f3#📌-sized-トレイト

dyn と、ここについてくる impl がわからない。

trait Trait {}

impl Trait for i32 {}

// old
// いままで
fn function1() -> Box<Trait> {}

// new
// これから
fn function2() -> Box<dyn Trait> {}

トレイトオブジェクトにトレイト名をそのまま使うのは悪手だった。

https://doc.rust-jp.rs/edition-guide/rust-2018/new-keywords.html

静的ディスパッチ - コンパイル時に呼び出すべきメソッドを決める、決まる。
動的ディスパッチ - 実行時にオブジェクトの型を調べ、対応するメソッドを呼び出す。

https://blog.ojisan.io/rust-dispatch/

トレイトだけを指定すると、その引数にやってくる実態が何かはコンパイラにはわからない。
動的ディスパッチを用いて、実行時にその実態を知ってもらうようにしてあげる。

関数がトレイトを実装した型を返す場合、 impl Trait という書き方ができる。

https://doc.rust-lang.org/rust-by-example/trait/impl_trait.html#as-a-return-type

関数の引数に &impl って書くやつと変わらんかったしたしかにそうだった。

いずれにしても全て本質的には返り値が違うので、注意しなければならない。
でも具体的にどう言う場面でどう注意したらいいかは思いつかない。

ゆでゆで

関数、クロージャ(4)

下記のコードはコンパイルが通らない。

fn put<T: std::fmt::Debug>(a: &T) {
    println!("{:?}", a);
}

fn main() {
    put("hoge");
}

動的サイズ型 は、実行時にサイズが決まる型で、str 型などはそれにあたる。
そうでない、つまり、型のサイズが予め推論できるとき、Rust ではその型を自動的に Sized トレイトのインスタンスにする。
つまり、関数の引数やジェネリック型などは Sized トレイトのインスタンスでなければならない。

str は動的サイズ型なので参照を付けていますが、これはエラーになってしまいます。
これは暗黙的に T: Sized となっているためで、引数側に参照を付けても TSized トレイトのインスタンスでなければなりません。
しかし、引数側で参照を付けているので、コードとしては動的サイズ型を指定しても問題ありません。
そこで、 T が動的サイズ型を受け入れられるように ?Sized を指定するこができます。

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/64c6f3#📌-sized-トレイト

ジェネリック型や関数の引数が Sized トレイトのインスタンスであり、 str は動的サイズ型なので、 &str としたとしても strT にはなり得ない(Sized トレイトのインスタンスになり得ない)ので、エラーになってしまう。
で、?Sized を指定する必要があるが、 ? ってなんすか。

これ系の解説記事を探していると、 fat pointer という言葉が時折出てくる。

自作スライスの説明で fat pointer という言葉が出てきた。
生ポインタと補助的な情報 (スライスなら長さ) を合わせた構造のことを指すらしい。
Rust 特有の用語ではなく、GC 本では dangling pointer の検出方法の一つとして紹介されている。

https://twitter.com/nhiroki_/status/1193664095297269760?lang=ja

"fat pointer" とは、動的サイズ型への生ポインタや参照のこと。
動的なサイズのヒープバッファを管理する型(Vec<T> など)も、コンパイラは Vec<T> インスタンスがスタックに占める正確なバイト数を知っているので、Sized である。
スライス ([T], str) や、トレイト (dyn Trait) は動的サイズ型である。

https://stackoverflow.com/questions/57754901/what-is-a-fat-pointer

Sizedを実装する型は、全て同じバイト数である。
C言語のsizeofに相当するstd::mem::size_of が使える。
(Sizedでない場合は値によって異なるため、std::mem::size_of_valを使う)

これらの値へのポインタは 16byte になる。
最初の 8byte にはデータの先頭番地が入っている。
続く 8byte には、スライスの要素数や、バイト数などが入っている。

https://qnighy.hatenablog.com/entry/2017/03/04/131311

fat pointer については大まかにわかった。

特別な記法 ?Sized は、Sized トレイトを実装しなくてはならないという制約を回避させてくれる。

https://doc.rust-lang.org/std/marker/trait.Sized.html

?<Trait> が特殊な文法なのでは?
と思ったが、コンパイラ怒られたので ?Sized 専用。

ゆでゆで

関数、クロージャ(5)

クロージャはトレイトであり、動的サイズ型であり、参照やBox型を使うことでオブジェクトとして扱えるようになります。
このようなオブジェクトをトレイトオブジェクトといいます。
トレイトオブジェクトを扱う側は実際のオブジェクトの型を知らなくても、そのメソッドを呼び出せるということ、
そしてオブジェクトの型によってメソッドの動作を変えられることになります。
これらの仕組みを 動的ディスパッチ といいます。

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/64c6f3#📌-静的と動的

動的ディスパッチをクロージャやトレイトの始点から語っている。
そして、以下のものが静的ディスパッチであることを述べている。

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

Box<&dyn Trait> で実装するものが動的ディスパッチで、 impl Trait で実装するものが静的ディスパッチ。
多分動的ディスパッチのほうが遅くなるだろう、という直感はあるので、制約を緩めたいときに動的ディスパッチを用いるという具合か。

impl Trait は抽象型ともいうらしい。

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/64c6f3#📌-ジェネリクスと抽象型

ゆでゆで

スレッド(1)

スレッドを作成、終了させるには、以下のようにする。

use std::thread;
let handle = thread::spawn(|| {
    // thread code
});

handle        // JoinHandle 型
    .join()   // Result 型
    .unwrap();

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/98dc80#📌-スレッド

クロージャの環境をスレッド間で共有することは通常の方法ではできません。
コピーを作成できるなら、環境にコピーされますが、そうでない(Vec<T> など)なら所有権を移動しなければなりません。
その場合は move || { /* thread code here */ } を使います。

複数のスレッド間で状態を共有するにはMutex 型で 排他制御 をする。
lock メソッドでリソースをロックし、 LockResult 型を返す。
LockResult 型は RAII である MutexGuard 型のオブジェクトを束縛しているので、自動でロックを解除する。

オブジェクトをスレッド間で共有するには、Rc 型のマルチスレッド版である Arc 型を使う必要があります。

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/98dc80#📌-スレッド

Rust のスレッドは OS スレッドで、Golang や Java などのような言語はグリーンスレッドと言い、違いとしては、仮想マシン上でマルチスレッドを実現するかというところらしい。

https://zenn.dev/tfutada/articles/16766e3b4560db#スレッド

非同期処理は下記のような形で行う。

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel(); // 非同期チャネルを生成

    thread::spawn(move || {
        tx.send(42).unwrap(); // (非同期)送信
        println!("Done immediately!"); // すぐにこの行に処理を移します
    });
    
    thread::sleep(Duration::from_secs(3)); // メインスレッドの処理を少し遅らせる

    println!("got {}", rx.recv().unwrap()); // 受信
}

https://zenn.dev/tfutada/articles/16766e3b4560db#非同期チャネル

同期処理は下記のような形で行う。
バッファサイズを 1 にすると、送信はブロックされないが、同期処理を行うらしい。

use std::sync::mpsc::sync_channel;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = sync_channel::<i32>(0); // バッファサイズを0にする
    thread::spawn(move || {
        tx.send(53).unwrap(); // 同期送信。受け取ってもらうまで待ちます。
        println!("Done after 3sec!"); // すぐには処理されません
    });

    thread::sleep(Duration::from_secs(3));

    println!("got {}", rx.recv().unwrap());
}

https://zenn.dev/tfutada/articles/16766e3b4560db#同期チャネル

  • バッファが いっぱいになると send はブロック される

https://scrapbox.io/nwtgck/Rustのstd::mpsc::sync_channel(bound)の引数boundは何か?

バッファのことがよく分からなかったので、ちょっと実験してみる。

let buffer_size = 0;

let (tx, rx) = sync_channel::<i32>(buffer_size);
thread::spawn(move || {
    tx.send(1).unwrap();
    println!("Done");
});

thread::sleep(Duration::from_secs(3));

println!("got {}", rx.recv().unwrap());
> got 1
> Done
let buffer_size = 1;

let (tx, rx) = sync_channel::<i32>(buffer_size);
thread::spawn(move || {
    tx.send(1).unwrap();
    println!("Done");
});

thread::sleep(Duration::from_secs(3));

println!("got {}", rx.recv().unwrap());
> Done
> got 1
let buffer_size = 1;

let (tx, rx) = sync_channel::<i32>(buffer_size);
thread::spawn(move || {
    tx.send(1).unwrap();
    tx.send(2).unwrap();
    println!("Done");
});

thread::sleep(Duration::from_secs(3));

println!("got {}", rx.recv().unwrap());
println!("got {}", rx.recv().unwrap());
> got 1
> got 2
> Done

バッファサイズが 2 になると、tx.send(2).unwrap(); で処理が止まっている(であろう)様子がうかがえる。

ひとまずスレッドはこのあたりで。

ゆでゆで

マクロ(1)

マクロの作成方法が気になっていた。

そもそもマクロには種類がある。

  • macro_rules! を使った 宣言的 (declarative) マクロ
  • 3種類の 手続き的 (procedural) マクロ
    • 構造体と enumderive 属性を使ったときに追加されるコードを指定する #[derive]
    • 任意の要素に使えるカスタムの属性を定義する、属性風のマクロ
    • 関数のように見えるが、引数として指定されたトークンに対して作用する関数風のマクロ

また、

関数シグニチャは、関数の引数の数と型を宣言しなければなりません。
一方、マクロは可変長の引数を取れます。
println!("hello") のように1引数で呼んだり、
println!("hello {}", name) のように2引数で呼んだりできる。

気にしていなかったが、言われてみればたしかにそうだ。

https://man.plustar.jp/rust/book/ch19-06-macros.html#マクロと関数の違い

下記は簡略化された vec! マクロである。

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

#[macro_export] 注釈は、マクロを定義しているクレートがスコープに持ち込まれたなら、
無条件でこのマクロが利用可能になるべきということを示している。

将来、Rustには別種の宣言的マクロが登場する予定です。
そのアップデート以降、 macro_rules! は事実上非推奨となる予定です。

https://man.plustar.jp/rust/book/ch19-06-macros.html#一般的なメタプログラミングのためにmacro_rulesで宣言的なマクロ

match 式のようなもので、正規表現のようなキャプチャをし、そのアームの先に書かれていることに基づいて式が展開されるという具合らしいが、非推奨になるならあまり深入りはしないことにする。

以降色々読んでみたが、あまり得たい情報は得られなかった気がするし、複雑なものは自分で定義するべきでもないような気もした。

ゆでゆで

なんやかんやで紙に書くお勉強に戻ってしまったので閉じます

このスクラップは2023/07/10にクローズされました