【2日目】Rust入門者に贈るRust入門者がRustと闘う物語 -普遍的なプログラミング概念-


最近、巷で話題のRust。システムプログラミング言語などの低レイヤー言語は触ったことがない筆者が、裸一貫で闘っていく、そんな物語です。

前回の記事

前回、指摘を受けて気付いたのですが、参考にしていたドキュメントが2016年のものだったらしく、2018年のものに変え修正しました。@tatsuya6502さん、ご指摘ありがとうございます🙇‍♂️

正直、分量が増えて焦ってますが、賽は投げられているので気合で前に進みます。

さて、本日は2日目ですが、3. 普遍的なプログラミング概念を参考にして学習を進めてまいります。

はじめに

※物語共通で使用するので不要な方は飛ばしてください。

開発環境や参考サイト、学習フローチャートなどの概要について話します。

本物語について

私は、今業務で使用している言語や趣味でしようしている言語を含め、使用している全ての言語を独学で勉強してきました(せざるを得なったためですが)。

そして、毎回のことのように何度も壁にぶつかり、やっとの思いで扱えるようになるため、プログラミング言語の取得は、修行のようなものだと考えています。

これは私自身の能力不足なのかもしれませんが、大小はあるにしても誰しもが共通して感じるものなのかなと思いまして、後にRustに入門する方々に参考になればと思い、物語として記録しておきます。

本物語の目的

・初心者としてありがちな凡ミスやくだらないエラーを赤裸々に記録し、後続する方々の参考にして頂くため
・自分用のメモ

本物語での記述について

ファイル名:わかりやすく日本語で書いていますが、自らの環境で真似するのはお勧めできません。

表現の置換:私の最も得意として楽に記述できる言語がJavaScript(特にES6の書き方が好きです)のため、多くの部分でJavaScriptで置換して表現します。多くの人がわかるようにPythonで置換しようと思ったのですが、Pythonの書き方は(個人的に)独特なので、これまた知名度の高いJavaScriptで置換しようと考えました。

エラーやつまづきポイント:敢えて全てのエラーの記録をお伝えしますし、遠回りの手法をお伝えすることがあります。これは私を反面教師にして同じ轍を進まなくて済むためと考えています。

※なお、全体を共通して「厳密性」より「なんとなく」を優先することが多々あるかと思います。その点ご了承ください。

対象読者

・今からRustに入門しようとしている方
・プログラミング自体はある程度できるけど、組み込み系やOS設計などの低レイヤーの部分は触れたことがないという方
・開発自体は好きだが、言語がある程度使えるまでの道のりがしんどい方

Why Rust

・業務でWebAssemblyが必要になりそうだった
・何か1つの言語を1から丁寧に学んでみたかった
・流行っていて今後本格的に低レイヤー言語としての地位を獲得しそうな雰囲気がした
・調べると、難しこと以外でデメリットが見当たらなかった
・名前が良い響き

開発環境とスキルセット

再現性は不明ですが、念のため記しておきます。

開発環境

PC: macOS Catalina 10.15.2
開発エディタ: VSCode
Rust: 1.41.0

スキルセット

基本的にWebフロントエンドとソフトウェア開発(金融系など)がメインですが、、

業務で使用
Javascript(デフォルト)(Node.js, Vue.js, React.js, Jquery, etc)
Python(主にAPIなどのバックエンド)(あまりフレームワークは使わない)
Swift(単純な業務アプリ等)
PHP(Wordpressカスタマイズとか頼まれたら)(比較的苦手)

趣味程度
Golang(速度が必要な処理でAPI作成)
Julia(計量経済学を学習した際に)
R(計量経済学を学習した際に)

主にWebのお仕事をもらいつつ、頼まれたら業務に必要なアプリ作ったりしています。
Swiftが一応システムプログラミング言語ですが、そこまで複雑じゃない業務管理アプリ程度しか作ったことがないので、システムプログラミングエンジニアとしては初心者です。
Golangは、Node.jsで速度処理に困った際に比較で学んだのがきっかけで業務での使用はないです。いつかお仕事できればいいな。

ちなみに、よく聞かれるので、JavaとRubyの使用経験についても記載しておきます。
・Java(Kotlin等も)は無茶振りで超単純なアプリを作ったくらいで、趣味にも入らないくらいです。
・Rubyは、変なセミナーに誘われることが多かったので怖くて触れてません。

学習スケジュール(予定)

そもそも私自身Rustを学ぶ目的というか必要がありまして、実は金融系ソフトウェアの開発にあたりWebAssemblyを使うかもしれない可能性があり、そのため最終的にWebAssembly系で何かできたらと思っています。

恥ずかしながら、こうしてスケジュールを組んで学習というのが初でして、いまいち書き方がわかりませんが、以下のようにしております。

基礎学習

n日目 やること
1日目 Rustと周辺ツールのインストールとセットアップ
2日目 3. 普遍的なプログラミング概念
3日目 4. 所有権を理解する
4日目 5. 構造体を使用して関連のあるデータを構造化する
5日目 6. Enumとパターンマッチング
6日目 7. モジュール
7日目 8. 一般的なコレクション
8日目 9. エラー処理
9日目 10. ジェネリック型、トレイト、ライフタイム
10日目 11. テスト
11日目 13. 関数型言語の機能: イテレータとクロージャ
12日目 14. CargoとCrates.ioについてより詳しく
13日目 15. スマートポインタ
14日目 16. 恐れるな!並行性
15日目 17. Rustのオブジェクト指向プログラミング機能
16日目 18. パターンは値の構造に合致する
17日目 19. 高度な機能

こちらを参考にして、スケジュールを組んでいます。

基本的にセクション毎を学んでいきます。量などにムラがあると思いますが、どうにかやり切る覚悟でいきましょう。

こちらはプロジェクト学習で最終的な復習をする前提で、一旦込み入ったことなどはスルーするかもしれないです。個人的に、80%くらいの精度で学習して、最後に実践的にボロボロになって、20%の総和を回収するのがなんだかんだ効率的だと思いますし、何より前に進んでいる実感からモチベが湧くので(ただ筋というかコアの部分は理解する必要があると思いますが)。

プロジェクト学習

n日目 やること
1つ目 2. 数当てゲームをプログラムする
2つ目 12. 入出力プロジェクト: コマンドラインプログラムを構築する
3つ目 20. 最後のプロジェクト: マルチスレッドのWebサーバを構築する

これらは1日でやり切れたらいいですが、文量なども多いので頑張れたらとしましょう。また、日数ではなくて、わからない点や疑問点をこのプロジェクトを通してゼロにしたいと考えているため、1つに1週間くらいかかることも想定されます。

付録

一旦スルーします。必要に応じて見ます。

応用学習

未定ですが、WebAssemblyで何かしたい。

参考サイト等(仮)

Rustチュートリアルがしっかりしてるサイト

何をやったか(2日目)

3. 普遍的なプログラミング概念

ざっくり要約

・量が多いので部分ごとまとめます。

3.1. 変数と可変性

追記:このセクションを終えて、一番大切だなと考えてます。

作業するディレクトリに、cargo new --bin variablesを走らせて、プロジェクトリポジトリを作成。

そして、src/main.rsを以下にしてcargo runを実行・

src/main.rs
fn main() {
    let x = 5;
    println!("The value of x is: {}", x);     // xの値は{}です
    x = 6;
    println!("The value of x is: {}", x);
}

エラーが出るはずですが、その理由が以下と説明されています。
不変変数xに2回代入できない
つまり、JavaScriptでいうconstですね。

次に可変性を持つ変数が登場(宣言はlet → let mut に変更)するらしいので、以下で試して見ます。

src/main.rs
fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

エラーが出なかったはずです。

つまり、JavaScriptの(ややこしいですが)let, varですね。スコープは今のところどんな感じかはわからないので、今後に期待ですね。

なお、英語的には、mutはmutability(実際は名詞かは調べてないです)から来ていて、mutabilityが変わりやすさとかの意味らしいですね。

というか、システムプログラミング言語のくせにSwiftとは違く、型宣言が不必要なんですね。ここら辺までは本当にJavaScriptって感じがします。

脱線と補足

mutキーワードについて:考えるべきトレードオフはバグの予防以外にも、いくつかあります。例えば、大きなデータ構造を使う場合などです。 インスタンスを可変にして変更できるようにする方が、いちいちインスタンスをコピーして新しくメモリ割り当てされたインスタンスを返すよりも速くなります。 小規模なデータ構造なら、新規インスタンスを生成して、もっと関数型っぽいコードを書く方が通して考えやすくなるため、 低パフォーマンスは、その簡潔性を得るのに足りうるペナルティになるかもしれません。

とありますが、ここの部分は結構大事な匂いがします。意味合いは違いますが、JavaScriptでは以下がよく比較されると思います。

比較.js
const objectA = {
a: null
}
//objectBの内容をobjectAと共通にする
//方法1:objectのシャローコピー, objectBもobjectAも全く一緒の扱いとなる
const objectB = objectA;
//方法2:objectのディープコピー, objectBはobjectAの内容を受け継ぎ別変数として誕生
const objectB = Object.assign({},objectA);
//方法3:objectのvalueだけ写す, objectBはobjectAのバリューを頂きます
const objectB = {}; // or new Object(); etc..
objectB.a = objectA.a;

とりあえずここら辺の話は僕はこれでイメージしました。

変数と定数の違い

定数というものの登場である。名前的に不変変数との違いに着目しましょう。

文章を読むと、グローバルスコープで宣言されるもので全くもって変更されないものが定数らしい。つまり、1m = 100cmの比率 1/100 とかが該当すると思います。

const MAX_POINTS: u32 = 100_000;のように宣言する。

個人的かもしれないが、JavaScriptでObjectKeyの型管理する時に使う手段です。

型は必要っぽいですね。

シャドーイング

この部分は興味深いです。

src/main.rs
fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("The value of x is: {}", x);
}

これが動くのです。面白いなと思いました。

あくまでletは再代入を禁止するイメージなんでしょうかね。宣言は複数回でき、それを覆い隠すと表現するのは中々センスがあると思いました。

その後に、

src/main.rs
fn main() {
//以下は成功
let spaces = "   ";
let spaces = spaces.len();

//以下は失敗
let mut spaces = "   ";
spaces = spaces.len();
}

とありますが、型変更は代入の定義を超越しているようですね。

なるほど、letで再宣言できるニーズがわかりました。

3.2. データ型

お、型についてですね。早速コードを写経してcargo runをしたら、、、

src/main.rs
let guess: u32 = "42".parse().expect("Not a number!");
result.sh
error: expected item, found keyword `let`
 --> src/main.rs:1:1
  |
1 | let guess: u32 = "42".parse().expect("Not a number!");
  | ^^^ expected item

えーなんでと思いましたが、すぐググってヒットしました。要は、関数内でしか使えないよってことですね。スコープの制限が厳しいと思いましたが、確かにエラーが無くなりそうでいいですね。

main関数がないとダメらしいのでつけて実行。

話を戻すと、この部分で説明されているのは、型推論はあるけど、複数の型が想像できるシチュエーションだとエラーになるから、そういう時は型宣言をしてねとのことでした。

src/main.rs
fn main(){
    //これは成功
    let guess: u32 = "42".parse().expect("Not a number!");  
    //これは失敗;数字か文字か推論ができないらしい
    let guess = "42".parse().expect("Not a number!");  
}  

とりあえず基本的には全部型宣言すればいいと判断し次に行きます。

データの型はたくさんあるのですが、基本的なのばかりなので、特殊なのを以下にあげます(ページurl)。

・文字型(char型, 文字列と違くユニコードのスカラーを表す。どうやら日本語はこっち?8章でやるみたいので楽しみにしておきます。)
・タプル(swiftと同じ感じ, 違う要素たちで構成されてもOK)
・配列(固定長のため変更不可、同じ要素で構成、代入は?ベクタ型で変長型は対応)

src/main.rs
fn main(){
    let mut a = [1,2,3,4];
    println!("{}", a[1]);
    a[1] = 3;
    println!("{}", a[1]);
}

配列だけ気になったので、試して見ました。無事、代入されました。ここら辺はJavaScriptのArray型と同じ性質を持つようですね。なお、let mut→letのように不変変数にするとエラーが起きました。letはJavaScriptのconstとかよりも強い制約があるようですね(JavaScriptでは、同一オブジェクトのためconstで通る)。

3.3. 関数の動作法

あ、今やるんですね笑

割と複数言語と共通点あるので、ある程度特殊だったり重要だと思う点を以下でまとめます。

以下にポイントをまとめます。

・関数名は、スネークケースで記述

・関数の宣言順番が緩い(つまり、関数A内から別関数Bを呼ぶ際にその別関数Bは呼び出し元関数Aより前に宣言される必要があるとかの制限がない。置き場所が自由)

各仮引数の型宣言が必須

・スコープはJavaScriptと同じで{(この中では、過去に宣言されていても別変数として宣言される。(既存のやつが上書きとかもされない))}感じです。てか即時関数みたいなのがあって興奮。

・戻り値は【関数(引数)->(型){(中身)}】として表現する。

・returnは省略可能で、その場合一番最後の式が暗黙的に返却される

・「;」の有無が「文」か「式」かを判別する。(「;」あるなら文)

このプログラムを実行すると、以下のようなエラーが出るでしょう:

本文.sh
$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
(エラー: 式を予期しましたが、文が見つかりました (`let`))
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^
  |
  = note: variable declaration using `let` is a statement
    (注釈: `let`を使う変数宣言は、文です)

このlet y = 6という文は値を返さないので、xに束縛するものがないわけです。これは、 CやRubyなどの言語とは異なる動作です。

とありますが、私はCもRubyも経験がないので、スルーしてしまいました。

3.4. コメント

地味に大事ですよね、コメント。

コメント.rs
// こうやってコメント

でできます。以上。複数コメントはどうやるのかはまた教えてくれるそうです(適当に/**/をやったらできたので、複数コメントできるようです。)。

3.5. 制御フロー

ここ重そうです。大まかに条件分岐とループなのでセクション分けして進めます。

条件分岐

一応他言語と大体一緒なので注意点だけ挙げます。

if, else if(pythonならelif), elseを用い条件分岐

・if ((条件式)) {(中身)}のように()は不要(警告文が出ました, JavaScriptだと必須なので一応)

if num {(中身)}(※num: i32とする)のような形はNG(JavaScriptだとundifined, nullとかを検知できる)

同一の型なら三項演算子ができる。(let number = if condition {5} else {6};はいけるけど、let number = if condition {5} else {"6"};は無理)

ループ

loopだけ紹介して、あとは基本的に他の言語にも搭載なので例の如く軽く列挙だけします。

loop
src/main.rs
fn main() {
    loop {
        println!("again!");   // また
    }
}

という、無限ループができる。

その他の特筆すべき点を列挙します。

・while, for文がある

・ループ強制終了はbreak;を使う(continueは謎)

・whileは条件式を評価してfalseになるまでループ

・for文はfor element in a.iter() {(中身)}(aは配列とします。というか配列はArray型でいいんだろうか。)でできる。

一旦ここで終了

冷静に分量が多いですね。プログラミング初学者は相当しんどいと思うのですが、申し訳ないです。他言語の経験があればある程度理解できたのではないでしょうか。

どうやらプチプロジェクトがあるみたいなので取り組んでみます。3は歌詞を調べるのが面倒で1,2だけ

プチプロジェクト1:温度を華氏と摂氏で変換する。

あるある問題ですね。データ型の演算についての問題でしょう。温度というのは、一瞬絶対温度のことかと思いましたが、恐らく華氏温度、摂氏温度のことを共通して表現しているものと解釈して、変換公式だけ実装しちゃいます。

華氏:f64(℉のやつ)
摂氏:f64(℃のやつ)

なお、(華氏) = (摂氏) * 1.8 + 32

src/main.rs
fn kasi_to_sessi(kasi: f64) -> f64 {
    (kasi - 32.0) / 1.8
}

fn sessi_to_kasi(sessi: f64) -> f64 {
    (sessi * 1.8) + 32.0
}

fn main() {
    let kasi :f64 = 53.6;
    let sessi :f64 = 12.0;
    println!("kasi_to_sessi result is: {}!", kasi_to_sessi(kasi));
    println!("sessi_to_kasi result is: {}!", sessi_to_kasi(sessi));
}

と実装しました。一応あえて関数使って、関数の勉強もしたつもりです。スコープをあえて勉強するとするなら、以下の書き方もできるでしょう。

scopeテストしたりしたやつ

rust:src/main.rs
fn main() {
let kasi :f64 = 53.6;
let sessi : f64 = {
//そもそも式だけでいいが、scopeの性質を知るためあえて
let sessi :f64 = (kasi - 32.0) / 1.8;
//return sessiでもいいし
sessi //;は要らない
}; //セミコロンは必須
println!("kasi_to_sessi result is: {}", sessi);
let sessi :f64 = 12.0;
let kasi : f64 = {
let kasi :f64 = (sessi * 1.8) + 32.0;
kasi
};
println!("sessi_to_kasi result is: {}", kasi);
}

これにたどり着くまでに二つエラーが出たので以下に記します。

int32とfn64の足し算がエラーとなる

int32に限った話ではないと思うのですが、型が違うとみなされるみたいです。

エラー内容.sh
no implementation for `f64 + {integer}`

標準出力(println!)が言うことを聞いてくれない

エラー内容.sh
error: argument never used
  --> src/main.rs:12:43
   |
12 |     println!("kasi_to_sessi result is: ", kasi_to_sessi(kasi));
   |              ---------------------------  ^^^^^^^^^^^^^^^^^^^ argument never used
   |              |
   |              formatting specifier missing

最初戸惑ったが、ドキュメントのコードをしっかり見ると、例えばprintln!("{}!", number);となっていて、pythonの置換されるやつぽいなと思い、{}!を入れ込んでみたら成功した。
恐らく最後の!はキャストのnull強制無視のやつだと考えていましたが、外したらエラーが出なかったので多分雰囲気作りでした。恥ずかしい。

プチプロジェクト2:フィボナッチ数列のn番目を生成する。

これもあるあるですが、他の入門系よりムズイのがn番目ってとこでしょうね。

そもそもn番目のフェボナッチ数列の一般項(今回求めたいもの)の公式は以下です。参考

\frac{1}{\sqrt5}*({(\frac{(1+\sqrt5)}{2})^n - (\frac{(1-\sqrt5)}{2})^n})

まあ、これは多分ライブラリ必要なので実直に実装しましょう。

フェボナッチ数列の数列の公式は以下です。初項(F1)を0とする場合もあるようです。本質的には変わらないので以下を使用します。

\begin{eqnarray*}
F_1 &=& 1 \\
F_2 &=& 1 \\
F_{n+2} &=& F_n + F_{n+1} \ \ (1 \leq n)
\end{eqnarray*}

普通に考えて、n回繰り返すのが良さそうなので、以下で実装しました。正直、フィボナッチ数列の関数は当分書いてないので、汚いと思いますが、動きます。

src/main.rs
fn fibonacci(n: i32) -> i32 {
    let mut pre = 1;
    let mut now = 1;
    for i in (1..n).rev(){
        let temp = now;
        now = now + pre;
        pre = temp;
    }
    now
}

fn main() {
    let n = 30;
    println!("fibonacci answer is: {}!", fibonacci(n - 1));
}

一応別方法

```rust:src/main.rs
fn fibonacci(n: i32) -> i32 {
let mut pre = 1;
let mut now = 1;
let mut count = n;
while count > 1 { // countが0になるタイミングの前に1回足されてしまう
let temp = now;
now = now + pre;
pre = temp;
count -= 1;
}
now
}

fn main() {
let n = 30;
println!("fibonacci answer is: {}!", fibonacci(n - 1));
}
```
正直、loopで内部でif文書いた方が理解しやすいかもしれません。

今回のエラー内容は以下です。

インクリメント系の++, --が使えない

エラー.rs
//大丈夫, エラーにならない
count -= 1;
//エラーになる
count --;

よくわからなかったこと、問題点など

・致命的な問題点などは今のところない

気をつけた方が良いこと

・型には気をつけて損はないですね(intとfloatの計算ができないのはしんどいです、、)
・letとlet mutとconstの使い分け
・式と文の明確な違いと使い分け(正直僕もまだわかってないですが、慣れの問題だと考え次に進みます)

まとめ

長いですね、、笑
12日22:45くらいからやって結構スピード感持って今やっと終わったので、先が思いやられます、、、
これが後20日弱続くとなるとしんどさを覚えますが、最後まで頑張ります。

さて、一通りやって、恐らく3.1. 変数と可変性が最も大切な箇所(他のところを疎かにするとかそういう話ではないです)なのかなと感じました。let mutだったり、定数だったりもそうなのですが、何よりメモリの部分が際立ってこれから意識していく感じがします(昔ベテランエンジニアさんによく怒られた)。特に、ハイパフォーマンスなコードにするために必須な知識だと思いました。これ以外の点は、クセがあるものの覚えれば大丈夫な部分ですが、ここだけは理解が必要なのかなと。

データ型が多すぎて特に疎かになってしまったので、寝る前に覚えて、明日もチェックするつもりです。あとプチプロジェクトいいですね。理解が早まります。

今日は最後の方とか色々雑になってしまったので、間違い等あったら申し訳ございません。明日は日中にスタートできるよう工夫して時間をしっかり費やしたいと思います。

余談ですが、本日日比谷でテリーギリアム監督の新作ドンキホーテを観たのですが、大変満足なので、コメディがいけてシュール系好きな方におすすめです。

次の記事

追加しました。こちら