195
195

Rustで勘違いしていたこと3選 🏄🌴 【100 Exercises To Learn Rust 🦀 完走記事 🏃】

Last updated at Posted at 2024-08-07

100 Exercises To Learn Rust を題材に Qiita Engineer Festa 2024 投稿マラソン に参加していました!

7月17日に仮置きを残しながら投稿したのち、 ようやく全記事完成しました! ので、その記念として、色々話したいと思います!(え、7/17ってもう3週間前...?!)

目次

※ 前半は100 Exercisesの話題が中心です。早く勘違い3選を見たいという方は こちら

もう一本まとめ記事を書きましたので良かったら読んでみてください!

【🙇 懺悔 🙇】Qiitanグッズ欲しさに1日に33記事投稿した話またはQiita CLIとcargo scriptを布教する的な何か

全記事一覧

どの記事も熱量を込めて書きましたので読んでいただけたら恐悦至極です :bow:

100 Exercises To Learn Rust を完走した感想

100 Exercises は比較的最近できたRustチュートリアルサイトです!100 Exercises自体の説明は初回に譲ります。(こちらでもそんなにしていないですが...)

マラソンの題材として完走した感想について、6月10日から約2ヶ月に渡り取り組んできたこともあり、エクササイズに対する思いを全て書き出すのは難しいですが、一言で表すと

:crab: 無理のないRust再入門 :crab:1

だったなぁと思います。つまり、筆者にとって「ちょうどいい内容」でした!

多くのRustaceanは至高の公式ドキュメント The Rust Programming Language (通称 TRPL) でRust入門するかと思います。TRPLは、Rustに留まらずコンピュータサイエンスにも足を突っ込んだ、かなり網羅的で詳細な入門書でとてもためになりますが、ただひたすら 長い です。Rustlings があれど全てを理解しながら進めるのは難しく、「とりあえず今は飛ばす」とした人が多いのではないでしょうか...?その後Rustプログラミングをしていく中で、我流で色々な知識を身につけていくわけです。

100 Exercises To Learn Rust はそんな我流で知識を身につけたRustacean(要は筆者)に復習の機会を与えてくれたのです!100 Exercisesの魅力を以下に挙げます。

100 Exercisesの魅力

1. そこそこの長さ

TRPLと比べて 長くない です。決して短いとまではいいませんが(何しろマラソン記事書き上げには2ヶ月かかりましたから... :sweat_smile: )、内容を読み問題を解いていくだけならば、体感でA Tour of Go 1.5周分ぐらい(?)(適当)の、復習に最適な長さに感じました!

2. 無理のない学習順序

TRPLは公式ドキュメントである以上、トピックの「必要性」よりは「重要性」を優先した順序で書かれていたような気がします。一方で、100 Exercisesは「 必要になったら 」解説するというスタイルなのが特徴的でした。まるで定義していない知識は使ってはいけない大学数学のよう...

特に顕著だったのは配列・コレクションとトレイト(他言語でいうインターフェースの機能です)の登場タイミングでしょうか...?Rustの理解において これがないと始まらないというのは、配列よりはどっちかというとトレイト です。それを踏まえてなのかはわからないですが、TRPLは先にコレクションを扱っている一方、100 Exercisesでは トレイトを先に 扱っていました。他言語から新しくRustをやる人にはTRPLの並びの方が親切かもしれませんが、解説していない内容を極力避けて、必要になった時に解説を行う、という100 Exercisesのスタイルは、特に再入門者である筆者にとって読みやすい順番だったと思います。

3. 各エクササイズ間に関連がある

前節に関係した内容ですが、エクササイズ全体を通した題材が用意されており、「 どういうシーン でその回で扱っている機能を使うのか?」というのがわかりやすい内容だったと思います。

特に3章から7章では、「チケット管理システムを作る」という内容でした。各章で以下のような機能を作ります。

章名 内容
3 Ticket v1 チケット構造体の定義。OOP的な内容を通して所有権・参照について扱う
4 Traits 主要なトレイトを扱い、チケット構造体への演算子オーバーロード等を行う
5 Ticket v2 列挙体を利用したバリデーション・回復可能なエラーハンドリング( Option 型や Result 型)を扱う
6 Ticket Management 静的・動的配列やその他コレクション( HashMap 等 )・イテレータ等を用いた複数のチケットを管理できるシステムの構築を行う
7 Threads 並行処理により、チケット管理システムをサバクラ化する

「前章・前節までで作ったものを元に新たな機能を実装する」というスタイルは、「課題(必要性)が出てきたから取り組む」という学びやすい仕組みを生み出しており、これも取り組みやすい理由でした!

4. 効率よく"ミソ"を摂取できる

いわば「 :surfer: :surfer: 夏期講習 :island: :island: !!!これだけは絶対に押さえておきたい要点n選!」みたいな感じで、解説対象がよく練られている印象でした。

例えば4章のトレイトでは CloneCopyDrop 等絶対に押さえておきたいトレイトによくページが割かれていたり、5章 ではRust言語それ自体の仕様ではないのにも関わらず、デファクトスタンダードとなっており避けては通れない thiserror クレートを紹介したりと、Rustaceanを名乗るには知っておかねばならないであろう知識を効率よく確認できる内容となっていました!

...というわけで、復習のまとめとして、次節では筆者が恥ずかしながら勘違いしており今回のマラソンで知ることができた知識を3つ紹介したいと思います!

Rustで今まで勘違いしていたこと3選

100 Exercisesを通して初めて知ったことは多々ありましたが、今回はその中でも厳選して3つ紹介します。

1. CopyClone::clone を...呼び出さなかった!

該当回: 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~

Rustにおいて「(容易に)複製できる値(型)」は、プリミティブ型かデータクラスか...ではなくて Clone トレイトを実装しているか否か ただその一言だけで表されます。筆者はこれに関連した記事を書いたことがありました。

拙著: Rustにはシャローコピーがわからない

Rustでは、Clone トレイトを実装している型の値 v に対して、その値を複製したい時は、 v.clone() と呼ぶことでディープコピーが可能です。

そして、この Clone トレイトに関連するトレイトとして、 Copy トレイトというものがあります。こちらを付けた型は「 .clone() を付けなくても複製を行なってくれる」ようになります。また、 Copy トレイトを実装する型は、必ず Clone トレイトを実装している必要があります。

以上をまとめましょう。関係性はこうです。

  • Clone トレイト実装型は .clone() で複製できる
  • Copy トレイトという .clone() なしでも複製してくれるトレイトもとい機能がある
  • CopyClone を前提とする

これらの事実より、筆者は勝手に「 Copy トレイトが付いていれば、複製時には .clone() を付けなくても裏で勝手に .clone() が呼ばれるんだろうなぁ」と思っていました。 違いました

Copy トレイトはヒープ保存を必要としない型にしか付けられないのですが、Copy が付いている型が複製される場合は「スタック上のメモリの値がそのまま別なメモリに転記される」ように最適化されており、 .clone() は呼ばれない というのが真実でした...100 Exercisesで知るまで勘違いしていた... :flushed:

PoCコード beep!は2回出力される気がするが、一度しか出力されない
#[derive(Debug)]
struct Hoge(u32);

impl Clone for Hoge {
    fn clone(&self) -> Self {
        println!("beep!: {}", self.0);    

        Hoge(self.0)
    }
}

impl Copy for Hoge {}

fn main() {
    let h = Hoge(10);
    let h2 = h;
    
    dbg!(h);
    dbg!(h2);
    dbg!(h);
    h.clone();
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=de1113871471d8e7deb6aeae09c48dc7

上記拙著にて間違った説明をしてしまっていたのですが、今回修正を入れました :bow:

また関連して、勘違いしていたわけではなかったのですが、「 Copy を実装する型は Drop トレイトを実装不可である」...というのも今回初めて知りました。

2. 不変参照は...実は不変ではなかった!

該当回: 【29】 チャネル・参照の内部可変性 ~Rustの虎の子、mpscと Rc<RefCell<T>>

勘違いというよりはよく知らなかったことになります。Rustの不変参照は名前通り値を変更できない参照ですが、 RefCellMutex を用いることで不変参照でも一時的に可変となることができます。これを 内部可変性 と言います。

筆者が知らなかったのは、不変参照が可変になれる分水嶺となる型 UnsafeCell の存在でした... RefCellCellRc の内部では皆 UnsafeCell を使っている らしいのです!

というのも、この UnsafeCellついていない不変参照は、コンパイラが最適化を施してくれる そうで、 内部可変性を持つ不変参照が最適化されてしまわないためには、 UnsafeCell の使用が必須 そうなのです!

ちなみに不変参照ではなく「不変な値(構造体)」なら、内部に可変参照を持てば(屁理屈的に)内部可変?になれます。その事実より不変参照も同様だろう(ただ、知り得ないちょっと特殊な unsafe をしているんだろう)と思い込んでいたので、このことを知った時は目から鱗でした...

3. 値がスレッドを跨ぐには 'static は...不要だった!

該当回:

こちらも勘違いというよりはよく分かっていなかった部分が鮮明になったという感じです。

Rustには、スレッド間で値を送り合えることを示す Send トレイトというものがあります。このマーカートレイトがついていない型は、レースコンディション等の問題でスレッド間転送ができません!そこまでは理解していました。

そして値(や参照)が普通の(←伏線です)スレッド間を跨ぐにはもう一つ条件がありまして、まさしくRustらしい話題なのですが、 'static ライフタイムを持つ...言い換えると、「以降プログラム終了まで永遠に生き続けられる」必要があります。なぜなら「お互いのスレッドがいつまで生きているかはわからない」ためです!

そして大体の場合、片方のスレッドが持つ値への参照というのは、その参照を受け取った側のスレッドから見るといつまで生きているかわかりません。 &'static T ならプログラム終了時まで生きている保証がされているのですが、 大体の場合参照は 'static ではない ので、「参照(あるいは 'static ではない値)はスレッド間を跨げない」と考えていました。

そう理解していたので、 Sync トレイトという Send に似た並行処理の話題で出てくるトレイトが理解できませんでした。というのも、 Sync の定義が以下だからです...

The precise definition is: a type T is Sync if and only if &T is Send.

和訳: 「型 TSync であることと、その参照 &TSend であることは同値である」

...:thinking: :thinking: :thinking:...???

どうせスレッドを跨げない参照が Send でも意味を為さなくないか ...????ドユコト...?!?」

ここで理解が止まってしまい、永遠に謎だったのですが、第28回 に登場した スコープ付きスレッド std::thread::scope がこの疑問を解決してくれました...

スコープ付きスレッドなら、「そのスコープより長寿な参照」を渡せます

Rust
use std::thread;

fn main() {
    let v = String::from("Hello!");
    let r = &v;
    
    thread::scope(|scope| {
        scope.spawn(|| {
            println!("{} 1", r);
        });
        scope.spawn(|| {
            println!("{} 2", r); 
        });
    });
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8a7c01287e4bfcf64bb700573ce2b2ee

「参照であるかどうか」と「転送可能であるか否か (Send か)」には関係がないことをこのメソッドのお陰で理解することができました! ('static でない) 参照でもスレッドを跨いで良い のです!

そう理解してから改めて「 Sync トレイトとはなんなのか...?」についても色々考えたのですが、それはぜひ第33回を読んでみてほしいです!

【33】 チャネルなしで実装・Syncの話 ~考察回です~

所感

100 Exercisesを通して、どうして自分がRustを好んでいるのか、また一段と理解できた気がします。

Rustが好きな理由は、「特にトレイトを始めとした型に関する知識・説明だけでどのような挙動をするかを端的に伝えられる」ことです。勘違い3選1つ目の Copy トレイトがわかりやすいですね。他言語だと新しい概念や構文を知る時に新しい文法や背景を知る必要があったりしますが、Rustでは「 ◯◯トレイトが実装されているから 」「 ◯◯型が使われているから 」の一言だけで理解できたり、伝えられたりすることが多いです!トレイトや型を通じて言語仕様を学べるという、この一貫したUIが好きなんじゃないかなと思います。

100 Exercisesを一通り完了させましたが、このエクササイズでは語られていないRustの深淵はまだまだたくさんあります、今後も精進していきたいです!(急な小並感)

ここまで読んでいただき、ありがとうございました!

  1. 本当にとんでもなくどうでもいい注釈ですが「無理のないバイオ」シリーズというのびハザ派生版のゲームが昔あってここの「無理のない」のニュアンスはこれ由来だったりします...他にもこの言葉が使われている例あったような気はするけど思い出せない...本当にどうでも良い注釈でした。

195
195
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
195
195