Rust勉強会

ワイ史上最強の言語! Rustをちゃんと理解してあげる

Rust Logo
↑ Rustのロゴ(開発者が自転車を趣味にしていたことからこのアイコンらしい。)


注意事項

  • 現時点でのRustのバージョンは1.43.1です。
  • 主催者はRust初心者です。資料もかなり雑です。(抑えるところは抑えているつもり。。)
  • 言うまでもありませんが、言語は手段です。「この言語さえ勉強すれば大丈夫!」という魔法の様な言語は存在しませんので、用法・用量を守って正しくお使いください。
  • とはいえRustは素晴らしい言語ですので、「Rustが好きすぎる病」が発症する可能性があります。ご注意ください。
  • 質問はいっぱいしてください!
  • 普段はこんなにしっかり資料を作りません。(今回はちょっと自己研鑽のためにも資料を作ってみました。)

勉強会の目的

  • Rustの立ち位置や使い所をなんとなく理解する。
  • Rustの言語仕様(特に所有権)をある程度理解する。
  • Rustを通じてモダンなプログラムの書き方(Option, Result, パターンマッチ)を理解する。(「別にRust使う予定無いし!」という方のためにもPythonやC言語などのサンプルコードも交えて説明します。)

今日のお品書き

1. Rustとは
ここでRustの全体感を話します。

2. Rustの文法
Rustの文法をリソース管理の部分と関数型の部分のみ説明します。

3. おすすめ開発手法
Rust開発環境や開発手法を説明します。

4. 実際にアプリを作る
TDDで簡単なアプリケーションを作ってみます。

5. おまけ:WebAssembly
WebAssemblyについて簡単に説明。

5. おわりに
まとめます。


1. Rustとは

まずはWikipediaを読んでみる

ざっくりとした情報を取得するには、Wikipediaはうってつけ。
Rustのキーワードを洗い出しました。

システムプログラミング言語

Rust(ラスト)はMozillaが支援するオープンソースのシステムプログラミング言語である。
Rust言語は速度、並行性、安全性を言語仕様として保証するC言語、C++に代わるシステムプログラミングに適したプログラミング言語を目指している。

システムプログラミング言語とは?

システムプログラミング言語というのは、物理的なハードウェアへのより直接的なアクセス手段を提供する言語。

  • オペレーティングシステム
  • ユーティリティソフトウェア
  • デバイスドライバ
  • コンパイラ
  • リンカ

基本的にはこれらを作ることが目的として設計されている。
そう言う意味では、アプリケーション向けの言語とは異なる。
(アプリケーション向きでないとは言ってない。)

システムプログラミング言語例

  • C
  • C++
  • Ada
  • D
  • Rust
  • Swift

オープンソース

プロジェクトはオープンソースのコミュニティベース開発で進行しており、言語仕様(検討段階含む)、ソースコード、ドキュメントはオープンソースライセンスで公開されている。

Mozillaっぽいよね。

マルチパラダイム

Rustはマルチパラダイムプログラミング言語であり、手続き型プログラミングオブジェクト指向プログラミング関数型プログラミングなどの実装手法をサポートしている。

基本的にはC言語に似ている。

関数型プログラミング??

手続き型やオブジェクト指向は有名だが、関数型プログラミングやその言語は聞き馴染みないかもしれない。
関数型言語は、関数を引数ないし返値として渡せる第一級関数として扱える。
詳しくは別の勉強会でやりたいが、関数型プログラミングの特長としては、こんな感じ。

  • 宣言的(定義的)にプログラムを書けるので(慣れたら)読みやすい
  • 代数的データ型を利用して抽象的なコードが書ける
  • 比較的安全な(副作用のない)コードを書ける

速度はC言語並み

コンパイル基盤にMIRとLLVMを用いており、実行時速度性能はC言語と同等程度である。

まぁ、とにかく速い。

LLVMって何さ?

TODO**

メモリ安全

強力な型システムとリソース管理の仕組みにより、メモリ安全性が保証されている。

強力な型システムというのは、「静的型付けかつ強い型付け」のこと。
リソース管理の仕組みというのは、「ボローチェッカー」のこと。

静的型付け?
プログラムを実行しながら型検査を行う => 動的型付け

a = 10 a = "hoge" # => OK!

プログラムを実行前に型検査を行う => 静的型付け

let mut a = 10; // => 型推論でこれはOK! a = "hoge"; // => NG!!

強い片付け?
実行時に型変換を許さない。

ボローチェッカー
Rustには、「所有権」という概念があり、一つのリソースは一つの所有者(変数)にのみ関連づけられる。
ボローチェッカーは所有権の競合および不正利用を検証する。

愛されている言語

Rustは2016〜2019年の間Stack Overflow Developer Surveyで「最も愛されているプログラミング言語」で一位を獲得し続けている。

4年連続1位!!


Rustの公式サイトをみてみよう

効率的で信頼できるソフトウェアを誰もがつくれる言語

なぜRustか?

パフォーマンス

Rustは非常に高速でメモリ効率が高くランタイムやガベージコレクタがないため、パフォーマンス重視のサービスを実装できますし、組込み機器上で実行したり他の言語との調和も簡単にできます。

ガベージコレクタとは?

Todo:

他の言語との調和?

FFIやWeb Assemblyについて
Todo:

信頼性

Rustの豊かな型システムと所有権モデルによりメモリ安全性とスレッド安全性が保証されます。さらに様々な種類のバグをコンパイル時に排除することが可能です。

所有権についてはあとで説明。

生産性

Rustには優れたドキュメント、有用なエラーメッセージを備えた使いやすいコンパイラ、および統合されたパッケージマネージャとビルドツール、多数のエディタに対応するスマートな自動補完と型検査機能、自動フォーマッタといった一流のツール群が数多く揃っています。

Rustでつくろう

コマンドライン

Rustの強力なエコシステムならCLIツールを素早く作れます。Rustはアプリのメンテナンスを信頼できるものにし、その配布も簡単です。

WebAssembly

Rustを使ってJavaScriptをモジュール単位で高速化しましょう。npmに公開しwebpackでバンドルすればすぐに使えます。

WebAssemblyとは??

Todo:

ネットワーク

予測可能なパフォーマンス。極小のリソースフットプリント。堅固な信頼性。Rustはネットワークサービスにぴったりです。

簡単に言うと、サーバーにもどうぞってこと。

組込み

低リソースのデバイスがターゲットですか? 高レベルの利便性を損なわずに低レベルの制御をしたいですか? Rustにお任せください。

Rustの活用事例

それぞれ確認してください。Firefox、DropBox、Cloudflareの事例があります。

参加しよう

Rustを読む

ドキュメントはめちゃくちゃ充実している。

Rustを観る

YouTubeも面白い。RFCの議論なんかしている。

コードに貢献する

今年中には、1プルリクを!

謝辞

個人
企業スポンサー

そうそうたる企業
(余談だけど、このページリロードするたびに企業の順番が変わる)

  • AWS
  • Google Cloud
  • Microsoft Azure
  • Mozilla
  • 1Password

TODO: wikiと公式だけからじゃなくて、記事も載せたい

2. Rustの文法

参考リンク

リンク 内容
The Book 文法が一通り書かれている。最初から全部読もうとすると大変かも。
Rust By Example コードで説明するタイプ。説明は少なめ。練習問題もあり。
標準ライブラリ Rust標準ライブラリガイド。最初から読む物じゃなくて、参照用。
CARGOブック RustのパッケージマネージャーCargoの説明。参照用。
RUSTDOCブック ドキュメントの作成方法や書き方の説明。参照用。
RUSTCブック RustのコンパイラRustcの説明。参照用。

TODO: 日本語ドキュメントを書く

他にも色々あるので、Rustを学ぶを確認してください。

この章では、The BookRust By Exampleを参考にしながら、説明します。

Hello, World!

まずは、はろわ。簡単。
Rustのエントリーポイントはmain()関数。
println!()というマクロで標準出力できる。

fn main() { println!("Hello, World!"); }
その他の出力系マクロ
  • format! (文字列をフォーマット)
  • print! (↑を出力)
  • println! (↑の改行あり版)
  • eprint! (print!のエラー出力版)
  • eprintln! (println!のエラー出力版)
フォーマットの使い方
fn main() { // 基本はこんな感じ。 println!("{} days", 31); // => 31 days // 数字を利用して、こんな風にも書ける println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob"); // => Alice, this is Bob. Bob, this is Alice // 引数を利用することも可能。 println!("{subject} {verb} {object}", object="the lazy dog", subject="the quick brown fox", verb="jumps over"); // => the quick brown fox jumps over the lazy dog // :を利用すると特別な値に変換できる println!("{} of {:b} people know binary, the other half doesn't", 1, 2); // => 1 of 10 people know binary, the other half doesn't // 空文字で埋めることも。 println!("{number:>width$}", number=1, width=6); // => 1 // 0埋めももちろん可能。 println!("{number:>0width$}", number=1, width=6); // => 000001 }

変数と可変性

Rustの変数は標準で不変(immutable)

fn main() {
    let x = 5; // letで変数宣言する
    println!("The value of x is: {}", x);
    x = 6; // コンパイルエラー!!
    println!("The value of x is: {}", x);
}

もちろん可変(mutable)な変数も扱える。
その場合は、mutというキーワードを利用する。

fn main() { let mut x = 5; println!("The value of x is: {}", x); x = 6; println!("The value of x is: {}", x); }
immutableの何がうれしいのか
  • 意図しない不具合を抑制できる
  • プログラムの可読性、保守性が向上する

TODO:

シャドーイングが可能。地味に便利。

fn main() { let x = 5; let x = x + 1; // <= シャドーイング! let x = x * 2; // <= シャドーイング! println!("The value of x is: {}", x); }

これは、変数を新たに定義しているので、再代入とは異なる。
新たな変数名を定義するまでもない時に便利。

データ型

スカラー型

  • 整数
  • 浮動小数点数
  • 論理値
  • 文字

整数型

大きさ 符号付き 符号なし
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

archというのは、アーキテクチャ依存という意味。
4バイト(32-bit) or 8バイト(64-bit)

fn main() { let x = 5; // <= デフォルトだとi32 let y: u8 = 1; // こんな感じに型を指定。 }

浮動小数点数

大きさ 符号付き
32-bit f32
64-bit f64

論理値
true or false簡単。
実は1バイトのメモリを食ってる。(理論上は1bitで済むはず。)

fn main() { let x:bool = true; let y = false; let z = 100 < 10; }

文字列

fn main() { let c = 'z'; let z = 'ℤ'; let heart_eyed_cat = '😻'; //ハート目の猫 }

複合型

  • タプル型
  • 配列型

タプル型
タプルは**複数の型(異なっても良い)**をはり合わせることができる型。
後述struct(構造体)と基本的には変わらない。

fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }

値の取り出し方

fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; // こんな風にパターンマッチで取り出せる println!("The value of y is: {}", y); }

アクセス方法

fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; // こうやってアクセス let six_point_four = x.1; // こうやってアクセス let one = x.2; // こうやってアクセス }

配列型
同じ型の値を複数まとめられる型。

fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; let where = a[10]; // <= 実行時にエラーになるよ。 }
Vec型との比較

Rustには、Vec型という
常に固定長の要素があることを確認したい時に有効。(Vec型との比較)

TODO:

関数

fnキーワードを利用して関数を宣言。簡単。

fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); }

引数をとるときは? 簡単。

fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {}", x); // xの値は{}です }

戻り値があるとは?

fn five() -> i32 { 5 // 最後の値が評価される // return 5; でもOK!! } fn main() { let x = five(); println!("The value of x is: {}", x); }

引数と戻り値の複合
簡単。

fn main() { let x = plus_one(5); println!("The value of x is: {}", x); } fn plus_one(x: i32) -> i32 { x + 1 // x + 1; と書くとコンパイルエラー }
式と文

文は、なんらかの動作をして値を返さない命令。
式は結果値に評価される。

fn main() { let y = 6; // これは式。 }
fn main() { let x = (let y = 6); // だから、これはNG!!(コンパイルエラー) }

ちなみに全ての言語が代入で値を返さないわけじゃ無い。
たとえば、Rubyでは代入で値が返る。(C言語も)

y = 10 # y = 10 が 10を返す x = y = 10

Rustで同じことやりたかったら、こう!

fn main() { let x = 5; let y = { let x = 3; x + 1 // => 値を返している }; println!("The value of y is: {}", y); }

コメント

飛ばします。あとでドキュメントコメントについてやるかも(TODO)

フロー制御

if式

書き方は一般的なif文と一緒。

fn main() { let number = 6; if number % 4 == 0 { println!("number is divisible by 4"); } else if number % 3 == 0 { println!("number is divisible by 3"); } else if number % 2 == 0 { println!("number is divisible by 2"); } else { println!("number is not divisible by 4, 3, or 2"); } }

ただ、式なので、値を返します。

fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {}", number); }

同じ型じゃ無いとダメ。

fn main() { let condition = true; let number = if condition { 5 } else { "six" // => 数値型と異なる!! }; println!("The value of number is: {}", number); }

loop
これはただの無限ループ

fn main() { loop { println!("again!"); } }

while
whileもあるよ。

fn main() { let mut number = 3; while number != 0 { println!("{}!", number); number = number - 1; } println!("LIFTOFF!!!"); }

for
forもあるよ。

fn main() { let a = [10, 20, 30, 40, 50]; for element in a.iter() { println!("the value is: {}", element); } }

Pythonのこんな書き方便利ですよね。

def main(): for number in range(10): print(number) for (index, val) in enumerate(["hoge", "fuga", "piyo"]): print(f"{index} : {val}")

Rustだとこう書きます。

fn main() { for number in 1..10 { println!("{}", number); } for (index, val) in ["hoge", "fuga", "piyo"].iter().enumerate() { println!("{} : {}", index, val); } }

所有権

本題にはいる前に

スタックとヒープ

実行時にプログラムが使用できるメモリ領域にはスタックとヒープの2種類がある

スタック領域

  • 高速
  • データは既知の固定サイズ
  • 秩序
  • last in, first out (push, pop)

ヒープ領域

  • 低速(スタックに比べて)
  • 可変サイズ
  • 無秩序
  • allocate して pointer を管理する

メモリーリークを防ぐ戦略

  1. 自力
    • C, C++など
    • メリット
      • ちゃんと実装すれば、高速。
    • デメリット
      • ちゃんと実装しなければ、死ぬ。
  2. ガベージコレクション(GC, ガベコレ)
    • ほぼ全ての言語が装備(Java, PHP, Python, Ruby, Go, Haskell
    • 実装方法
      • 参照カウント
      • マーク・アンド・スイープ
    • メリット
      • メモリ管理を意識する必要がない。
    • デメリット
      • 自力戦略に比べると速度は劣る。
      • 場合によってはStop the world?(現代のGCでは改善されているかも。)
  3. 所有権
    • Rust
    • メリット
      • CやC++と同じくらい高速
    • デメリット
      • 学習コスト?(ボローチェッカーで管理してくれるものの)

所有権はRustの最もユニークな機能であり、これのおかげでガベージコレクタなしで安全性担保を行うことができるのです。 故に、Rustにおいて、所有権がどう動作するのかを理解するのは重要です。

所有権のルール

  1. Rustの各値は、所有者と呼ばれる変数と対応している。
  2. いかなる時も所有者は一つである。
  3. 所有者がスコープから外れたら、値は破棄される。

変数スコープ

スコープの概念は他の言語と同じ。

{ // sは、ここでは有効ではない。まだ宣言されていない let s = "hello"; // sは、ここから有効になる // sで作業をする } // このスコープは終わり。もうsは有効ではない

まとめると、

  • sがスコープに入ると、有効になる
  • スコープを抜けるまで、有効なまま

3. おすすめ開発手法

俺氏が1週間で考えた最強の開発手法(多分一般的です。)

こういうの、大事。

0. インストール

まずは、rustupコマンドを取得する。
これでcargoコマンドも一緒に入る。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

最新のRustをインストールしておく。

rustup update

1. VSCode + RLSを使う

やっぱりVSCode最強説。軽いし、いろんなプラグインがある。
TODO: VSCodeプラグイン作る勉強会やりたい。

RLSを入れると無敵。
一つだけ、Rustパスを設定するのを忘れずに。

/Users/hikaru/.cargo/bin/rustup
  • TDD
  • Cargo watch
  • cargo clippy
  • cargo make

4. 実際にアプリを作る

コマンドラインツールでいいかな。

5. おまけ:WebAssembly

WebAssemblyについて少し。
TODO:Yewで何か作ってみる?

wasmとは

6. おわりに

まとめ。

  • Rustを勉強すると、組み込みやってみようかな。とか、WebAssemblyやってみようかなとか思う。
  • めちゃくちゃドキュメントやサイトに力を入れている

メモ

data types

  • option
  • result
  • iterator

control flow

  • match
  • iteration

void f(Foo* ptr) { if(!ptr) { return; } ptr->g(); }

いけてないところ

  • nullチェックしてる
  • ptrが変更される可能性がある
  • ptrが無効な値の可能性がある
fn f(ptr: Option<&Foo>) { match ptr { Some(ptr) => ptr.g(); None => {} } }

エラーハンドリング

まずはpanic!を理解

fn exit(x: i32) {
    if x == 0 {
        panic!("we got a 0")
    } else {
        println!("things are fine!")
    }
}

fn main() {
    exit(1)
    exit(0)
}

ResultOptionを理解する

enum Result<T, E> {
    Ok<T>,
    Err<E>,
}

enum Option<T> {
    Some<T>,
    None
}
fn exit(x: Option<i32>) {
    match x {
        Some(0) => panic!{"we got 0"},
        Some(x) => println!("we got a {} things are fine!", x),
        None => println!("we got nothing!")
    }
}

fn main() {
    exit(Some(1))
    exit(Some(10))
    exit(None)
    exit(Some(0))
}

ファイルの入出力

use std::fs::File;

fn main() {
    // returns Result<File>
    let res = File::open("text.txt");
    
    let f = match f {
        Ok(file) => file,
        Err(e) => {
            panic!("{}", e);
        }
    };
}

こんな風にパターンマッチも可能

use std::fs::File;

fn main() {
    // returns Result<File>
    let res = File::open("text.txt");
    
    let f = match f {
        Ok(file) => file,
        Err(ref error) if error.kind() == ErrorKind::NotFound => {
            match File::create("text.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("culd not create file: {:?}", e),
            }
        },
        Err(error) => panic!("culd not open file: {:?}", error)
    };
}

関連リンク

言語に関するもの

記事

ニュース

その他