「コンセプトから理解するRust」を読んでRustに入門する
3年前に抱いていた Rust に対するイメージ
「コンセプトから理解するRust」(Amazon)という書籍を読んで Rust に再入門してみました。 実は2019年に Rust のチュートリアルである「The Rust Programming Language」(通称 TRPL)を写経したことがあります(GitHub)。
しかし、当時の自分は PHP のバックエンドエンジニアとしての経験が2年ちょっとあるくらいで、TypeScript にもまだ入門しておらず静的型付け言語の経験はほとんどなかったように記憶しています。
当時から Rust は所有権などの概念が難解で初心者を拒むものの、 Stack Overflow のアンケートで開発者から人気な言語として高い地位を誇っているというイメージがありました。
自分自身、PHP 以外にもサーバーサイドで使える言語があるといいなという思い、Rust は学習が難しいものの一度学べば資産となり他でも使える考え方を得られるかもしれないという考え、Rust に難しいものを学んでみるというちょっとした憧れみたいな感情を持っていました。
ただ、2019年春頃時点では日本で Rust を使っているメジャーな会社がなく、転職サイトでヒットしたのは外国の方が日本で設立した仮想通貨関連の会社くらいでした。今とは全く状況が違いますね。このため、がっつり使うまで学ぶというより勉強のためにやってみようかな、でも難しそうだなと二の足を踏んでいました。
「The Rust Programming Language」で Rust に入門した
そんな折、「六本木ではたらくソフトウェアエンジニアへのよくある質問とその答え (FAQ) 」 という Rust を支持する記事を読み、やっぱり Rust って良いんだ思いました。
それをきっかけに冒頭で紹介した TRPL を写経していったのですが、文章が難しくて内容が頭に入りづらかったです。文章を読み、サンプルコードを見ながらそれをエディタに入力し、手元の環境で実行。一つ一つの章を完璧に理解するよりもまずは概観することを優先し、腑に落ちないところがあってもとりあえず先に進むようにしました。
スタックやヒープ、enum、スライス、ハッシュマップと知らない単語がたくさん登場する上に、特に文字列の扱いには困りました。Rust には文字を扱う型に str、String、char があります。なんでこんなに大変なんだろう、””
で囲むだけの PHP は楽だと思ったものです。
一章進んでは知らない単語を調べ、コードを写経する。タイポしては長いエラーメッセージに驚き、エラーを読んで理解できるところは自分で修正し、わからないところはサンプルコードを見て答えとする。これにとても時間がかかりました。
また、友人が Rust でミニプログラムを作ったという話を聞いてすごいなあと思いつつ、自分もやればできるはずと気合を入れて TRPL を一通り終わらせました。
TRPL 1周目でわかったこと、わからなかったこと
最後の方は疲れ切っており、よく理解せずに写経するだけになっていました。それでも、TRPL のおかげで理解できることもありました。
値を変数に束縛し(代入じゃない!)、スコープを抜けたらメモリから値を解放すること、値の参照があること(先輩からPHPではご法度と教えてもらっていました)、構造体とメソッドは別で定義するため、データとそれを操作するメソッドをクラスとしてまとめるオブジェクト指向とは書き方が異なること、初心者の最初の関門とされている所有権と借用の考え方も理解はできました。初心者には所有権よりライフタイムの方が難しいだろうと思いました。
他にも正の整数の型である usize 型があったり、match式や if let が便利なこと、null がないことやドキュメントを自動生成できること、テストや formatter がビルトインであることなどはちゃんと頭に残っていました。
しかし、それでもわからなかったことの方が多いです。 <T>
のように書くジェネリクス(関数定義のどこに書けばいいか覚えられなかった)、トレイトやトレイト境界、スライス、ライフタイム、Box、Rc、RefCell、Arc、Mutex、クロージャ、イテレータ(とりあえず !vec を iter())。メソッドの第一引数の &self ってなんだろう。なんであったりなかったりするんだ。Option と Result<T, E> はわかったようなわからなかったような感じ(とりあえず unwrap する)。
わからないことは多かったのですが、それでもいい影響はありました。 可変と不変の考え方を知ったので、PHP でも変数の再代入を極力避けるようになりました。型を書くことは面倒なのではなくプログラムが堅牢になるとのだとわかったので、引数と返り値の型が書ける場面では必ず書くようにしました。
TRPL を終えた後、Rust で TDD の Bowling Game を実装してみました。ただ、それでもちゃんと理解したという感覚は得られなかったので、人には Rust を触ったことがあるとは言えませんでした。
その間にもネットで Rust の記事が増えているのをみて使用人口の増加を感じたり、Twitter で Rust に入門したという人が増えていくのをみて素直にすごいなと思いました。一方、それを見るたびに自分は再トライしてもまだ理解できないんだろうなと思っていました。
再入門のために「コンセプトから理解するRust」を読んだ
しかし、個人的なことながら年初に2022年の目標は「深掘りすること」と決めていました。 また、フロントエンド周りでは SWC や Rome をはじめ、Rust で書かれるツールが年々増えています。そこでまずは、Rust と同様に入門に挫折したことのある GraphQL の書籍を先日読んでみました。 この書籍はスムーズに読み進めることができたため思わず自信を得ました。
それがきっかけで、Rust も今ならわかることがあるのではないか、少なくともライフタイムとトレイトくらいは理解してみたいと考えました。 しかもタイミング良く初心者向けの書籍が発売される上、その書評 にも以下のように書かれており期待できます。
「所有権」や「ライフタイム」あるいは「トレイト」など、最初他の言語から Rust に入門すると理解に苦労する概念をとくに丁寧に説明しています。
ということで「コンセプトから理解するRust」を買って読んでみました。Software Design の自分の文章をチェックしてくださった編集者の @akito912912 さんが本書を担当したということも購入を後押ししました。
「あれ?今は理解できる」という感覚でどんどん読み進めていった
以前わからなかったライフタイムとトレイトを学ぶため、先にその章から読み始めました。すると、驚いたことに以前と違ってよく理解できました。 これはもしや...と思って Option、 Result の箇所を読んでもコードを理解しながら読めました。これはひとえに本書の丁寧な解説によるおかげです。
特に str 型と String 型の違いを解説する次の一文を読んで目から鱗が落ちました。
配列方の高機能版としてベクター型があったように、str型の高機能版としてString型があります
配列(固定長)とベクター(可変長)の解説を事前に読んでいたので、これはわかりやすい解説だと感じました。その他にも変数と値の関係の比喩も、よくある箱ではなくラベルで解説されており、とても理解しやすいものでした。
本書にはこのように「この一文があるから Rust に対する理解が捗った」と思うような表現がたくさんあるので、次にいくつか引用します。
なお、ここでは全く関係ないことですが、「目から鱗が落ちる」という諺はイタリア語で「目からハムが落ちる」というそうです。
Rust の理解を助ける表現を書籍から引用する
所有権
既存の変数を新しい変数と = で結びつけたときに、新たなメモリ領域が確保され、既存の変数の値を新たなメモリ領域にコピーする方式を「コピーセマンティクス」と呼んでいます。
既存の変数を新しい変数に = で結びつけたときに、新たなメモリ領域を確保せず、所有権を移動させて新しい変数のラベルに付け替える方式を「ムーブセマンティクス」と呼んでいます。
所有権という考え方を導入すると、メモリ上に格納された値とラベルである変数が1対1に対応します。
プリミティブ型には、Copyトレイトが実装されていて、ほかの変数に束縛しても所有権を失わないことが共通した特徴です。
借用
値の代わりにリファレンスを関数の引数に渡しても、所有権が関数内の変数に移動しない
リファレンスのスコープは参照する元の変数のスコープよりも小さい必要があります。これを所有権の言葉で言い換えれば、「リファレンスは参照する元の変数の所有権のライフタイムを超えて使うことができない」ということになります。
スライス
スライスはリファレンスであり、別に実体があるものに対する「ビュー」を与えています。所有権は持ちません。
文字列リテラル
文字列リテラルに束縛された変数は &’static str 型と推論されます。
文字列リテラルが示す文字列はプログラム実行の初期にメモリの「静的領域」と呼ばれる領域に配列のように配置され、そのスライスとなります。
「静的領域」はプログラムの起動から最後まで割り当てられていてサイズが不変な「静的変数」を配置する領域です。
構造体
クラスを持つ言語では、クラスの宣言の中で変数やそれを操作するメソッドを定義しますが、Rust では構造体の関連関数やメソッドの定義は別のブロックで行われます。
Box
生ポインタを使わずにヒープ領域にデータを配置できる Box型があります。
Box型を用いるもう1つの場合が、コンパイルのときには方が決まらない場合
トレイト
ある動作に関連するメソッドを列挙したものを「トレイト」と呼んでいます。類似のものにJavaやGo言語の「インターフェース」、Haskellの「型クラス」があります。
型パラメータに加える「トレイトを満たしている」という条件のことを「トレイト境界」と呼びます。
ある型がトレイトを満たすためには、トレイトで宣言されたすべてのメソッドがその型に実装される必要がありますが、デフォルトの実装があるメソッドはすでに実装済みになります。
その他
型やライフパラメータを固定せずにパラメータ化し、構造体や関数・メソッドを定義することをジェネリクス(generics)と呼んでいます。
Rust に入門して破門されたが、その後の学習が穴を埋めた
「コンセプトから理解するRust」の読書体験は自分にとって記憶に残るものとなりました。 本書を読み進めて自分に理解できることが増えていることに安堵すると同時に、実は3年前の自分に足りていなかったのは Rust に対する理解というより、Rust が取り入れている概念なのだと気付いたからです。
ここからは個人的な体験ですが、印象深かったので書き残しておきます。
例えばジェネリクス。ジェネリクスは TypeScript を通して学びました。例えばトレイトとトレイト境界。これはインターフェースと抽象クラスを PHP で学んでいたので理解できました(なお、PHP にも Trait はあります)。
他にも HashMap は JavaScript の Map で、イテレータはデザインパターン(GoF本、Amazon)のイテレータパターンで、map や reduce を多用する使う関数型のプログラミングの作法は React から、メソッドの第一引数に &self で構造体自身を受け取れるのは Python のクラスから学びました(以前 AtCoder に挑戦していた時期があり Python を少し触ったことがありました)。
また、Result<T, E> については JS Conf 2021年の「関数型プログラミングのデザインパターンひとめぐり」 という発表のスライド6ページ目から、例外が発生する可能性がある関数で値またはエラーをラップして返すことができるということを知りました。なお、発表内容は自分にとって難しく全然理解できていませんが、少しでも役に立つところがあったので聞いてみてよかったと思いました。
文章に書いてしまうと3段落ですが、背後に積み重ねがあったから今スムーズに本を読み進められると思うと感慨深いです。
まとめ
Rust に入門したけど理解できず破門されたと思って遠回りした結果、Rust で取り入れられている考え方をそれぞれの別の場所で学び、結果的に Rust を理解できるようになっていました。 本書は Rust を理解するための最後の1ピースだったのです。
学習では一般的に、「基礎を固めて初めてと発展的な内容が理解できる」と言われています。学校でよく聞いた表現ですね。これは、知識が互いにリンクしていることを表しています。ある知識は別の知識と関連しており、直接、間接を問わず複数の知識の上に新たな知識が生まれるものです。
そして、ある領域で得た知識が別の領域で活かせる場合、それは学習資産と呼べるでしょう。学習コストと学習資産の話は @lacolaco さんの 「Angularの学習コストは本当に高いのか?」という投稿で例とともに端的に解説されています。
記事の内容は、Angular とそれを取り巻くツールを一通り学ぶには時間と労力がかかる。得た知識と経験はAngular でしか活かせないものと他でも活かせるものに分けられ、後者の要素が多いため学習が大変でも学ぶ価値があるというものです。
Rust も同様に、モダンなプログラミングのベストプラクティスを取り入れたような言語設計となっているため、学習コストは大きくとも学習者にとって今後の大きな資産になるでしょう。
2022年時点では無料で公開されている Rust の資料も多いです。@helloyuki_ さんの 「Rust を始めるための資料集」 という記事が良質なリンク集となっており、ここから自分に合う教材を探してみても良いでしょう。
さて、世の中の流れにはトレンドとサイクルの2つがあります。トレンドは優勢ならさらに優勢、劣勢ならさらに劣勢と一方向に進む動きです。サイクルは定期的に人気・不人気などを繰り返すものです。サイクルの例を挙げると「あるツールがたくさん生まれた後に有力な一つが覇者となり、そのツールに不満が溜まるとその代替ツールが雨後の筍のように生まれてくる」というようなケースです。
Rust が広まっていくことはサイクルではなくトレンドでしょう。これから先も Rust の使用人口は増え、採用される領域が広がっていくことと思います。Vercel に勤務している Lee Robinson 氏は自身のブログで「Rust は JavaScript のインフラの未来を担うだろう」と書いています。
本業で使う使わないに関わらず、今が Rust を学ぶ良い機会なのではないでしょうか。最後に、本書の中で Rust を学ぶにあたり勇気づけられる一文を紹介して、筆を置こうと思います。
一発でコンパイルエラーがないコードを書こうと思わず、コンパイラのエラー情報の助けを借りながらコードを書き進めることが Rust のコードが書けるようになるコツです。
これは自戒ですが、本を読んでわかったというだけでは真に理解したとは言えません。コードを書き、コンパイラとの対話を通して、Rust に対するメンタルモデルを構築していきましょう。
単語集
Rust、cargo、クレート(crate)、モジュール、所有権、リファレンス、ライフタイム、コピーセマンティクス、ムーブセマンティクス、束縛、ミュータブル、イミュータブル、変数の再宣言、シャドーイング、プリミティブ型、スライス、構造体、フィールド、メソッド、Option、Resukt<T, E>、match 式、if let、HashMap<K, V>、Vec、Box、ジェネリクス、動的ディスパッチ、静的ディスパッチ、ゼロコスト抽象化、トレイト、トレイト境界、マクロ、イテレーター、map、filter、fold、reduce、並列実行、非同期処理
Happy Coding 🎉