[社内勉強会レポート]『The Rust Programming Language』勉強会#1

Rustの入門書『The Rust Programming Language』の社内勉強会レポートです。
2019.08.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

社内で行われた勉強会のレポートです。参加者のRustの経験は実務で少しずつ導入している人からRustの本を買って積んだままにしている人まで様々です。

クラスメソッドのSlackには様々なチャンネルがあり、その一つRustについて話す所があります。そこで社内勉強会を行うことになったのでそのレポートを記事にします。

Developers.IOでは社内勉強会についての記事は過去にテスト駆動開発の読書勉強会などのレポートなどが公開されています。この勉強会のレポートもそれを踏襲します。

TRPLについて

今回勉強会のテーマになっている『TRPL』はRustが公式に提供している入門書です。The Rust Programming Languageの略です。the bookとも呼ばれています。

日本語版もありますが、日本語版は最新のバージョンに追従していない部分があり、英語版を適宜参照しながら進めていく形になりました。

Rustについて

検証環境

$ rustc --version
rustc 1.36.0 (a53f9df32 2019-07-03)
$ cargo --verseion
cargo 1.36.0 (c4fcfb725 2019-05-15)

開発環境構築、基本的な使い方

事前に各々自由に開発環境を作っていましたが、私はJetBrainsのIntelliJのcommunity版にRustのプラグインを入れてコーディングの環境を整えました。

※参考

プラグインの導入はPreferenceから行えます。加えて保存時にフォーマッタが走るように設定しました。

必要なCLIツールのインストールもコピペで終了です。

$ curl https://sh.rustup.rs -sSf | sh

Hello worldまで

プロジェクト用のディレクトリを作ってrustcコマンドを叩くとコンパイルに成功した場合実行ファイルが生成されます。

$ touch main.rs
$ vi main.rs

main.rsの中身

fn main() {
    println!("Hello, world!");
}

コンパイルと実行

$ rustc main.rs

コンパイラがコンパイルに成功したら実行可能なバイナリを出力します。

実行は以下のようにします

$ ./main
Hello, world!

きちんと出力されました。

余談ですがSwiftもRustと同じ事前にコンパイルが必要な言語です。swiftcというコマンドがあり、同じような方法で実行ファイルが生成できます。

$ swiftc -o hoge.out hoge.swift

Cargo

CargoはRustのビルドシステムであり、パッケージマネージャであり、RustaceanはCargoをRustプロジェクトの管理にも使います

他の言語だと複数のツールを使って実現することをCargo一つでやっている印象を持ちました。

パッケージの追加や削除といった作業はプロジェクトのルートディレクトリ直下のCargo.tomlを編集して行います。

[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]

[dependencies]

日本語版ではこのように記載されていますが、英語版では以下のように

[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"

[dependencies]

editionというのが追加されています。

Edtion

Rustのstable版は後方互換性を高く維持してきましたが、絶え間なく改良されていくコンパイラの設計や言語仕様は互換性に影響のある変更が含まれることがあります。そのような非互換の変更を扱うための仕組みがeditionです。

Rustの公式ブログでも案内がなされているので興味のある人は読んでみてください。

Cargo buildコマンドを叩くと

  • targetディレクトリが出来てその下にビルドバリアント名のディレクトリ、その下に実行ファイルが生成されます。--releaseでリリースビルドが行えます。Swiftのコンパイラもそうですが、ビルドバリアントによって最適化のされ方等が異なります。

実行は$ cargo runコマンドです。

Cargoでプロジェクトを作成する時は

$ cargo new project_name

newコマンドを使用します。日本語版には--binオプションが指定されていますが、最新版ではbinオプションを指定しなくても実行可能なアプリケーションを作成するようになっています。--libオプションをするとライブラリ用のプロジェクトが生成されます。

今回はアプリケーションの作成のみできれば十分でしたが、せっかくなので--libオプションをつけてnewコマンドを叩いてみます。違うのはsrcディレクトリです。

drwxr-xr-x  9 hoge  staff   288B  8  1 23:44 .git
-rw-r--r--  1 hoge  staff    30B  8  1 23:44 .gitignore
-rw-r--r--  1 hoge  staff   247B  8  1 23:44 Cargo.toml
drwxr-xr-x  3 hoge  staff    96B  8  1 23:44 src

--libオプションを付けるとsrcディレクトリの中身がlib.rsになります。

中のコードは以下のようになっています。

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

数当てゲームを作る

導入に関する説明のあとは数当てゲームを作ってみる章に入りました。 

数当てゲームをプログラムする - The Rust Programming Language

順番に進めるだけでこのまま記事にするとただのリライトになるのでここからは学習を進めつつ残したメモや理解するために別で調べる必要のあった概念などを列挙していきます。

マクロ

hello worldで出てきたprintln!もこれにあたります。

構文レベルでの抽象化を可能にするメタプログラミング機構です。マクロ呼び出しは展開された構文への短縮表現です。展開はコンパイルの初期段階、全ての静的なチェックが実行される前に行われれますが、マクロの実装は頻繁に行なって良いものではなく最終手段となる機能と位置付けています。マクロを使用することで適切な抽象化が可能になる場面があるものの、良いマクロの設計が困難なためとドキュメントでは説明されています。

トレイトによって実装を強制されるメソッドを定義したい時にIDEの機能で実装しないといけないメソッドの雛形を用意してくれるのですがその時にもマクロが使われていました。

impl HasArea for Circle {
    fn area(&self) -> f64 {
        unimplemented!()
    }
}

panic!と呼ばれる回復不能なエラーを発生させ、プログラムにエラーメッセージを表示させて終了させるマクロの一つです。unimplemented!は"not yet implemented"というメッセージを表示させます。Swiftで同じようなことをしようと思ったら

func hoge() {
  fatalError("not implemented")
}

とかでしょうか。ちなみにprintln!は標準出力に一行分の文字列を出力させるマクロです。

Rust標準マクロのソースコード

Rustの標準マクロは一つのファイルにまとめられています。

let mut

use std::io;
fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");
    println!("You guessed: {}", guess);
}

サンプルコード中に以下のように変数を宣言する行があります。

let mut guess = String::new();

Rustでは変数は標準でimmutableで変数名の前にmutをつけて変数を可変にできます。 ::という記法で左辺の型の関連関数newを呼び出していることを示しています。 この場合Stringの関連関数newを呼び出すことでmutableな空の文字列オブジェクトを生成しています。

実装していくのは数当てゲームなのでこの後このmutableなString型のオブジェクトがユーザーの入力を受け取って入力された文字列が代入されます。

入力を受け取るメソッドread_lineのシグネチャがpub fn read_line(&self, buf: &mut String) -> Result<usize>になっていて第2引数は&mutがついたString型となるのでここに渡すための変数を先程宣言していたのだと理解することができます。

ここでの返り値がResultという型になっています。エラーが発生する可能性のある操作に対しよく使用される型です。Swiftでも5.0からResult型が使用できます。同じような用途でEitherという型が用意されている言語もありますね。モダンと言われている言語には類似した命名の型や言語機能があることが多いので比較しながら理解しようとするのも楽しいです。

アトリビュート

Rustで型が提供しなければいけない機能をRustのコンパイラに伝える言語機能をトレイトといいます。

println!というマクロを使用する時に自分で定義した型を出力しようとするとDisplayというトレイトを満たしていないと出力できない旨をコンパイラが教えてくれます。ここで初めてトレイトという概念を知りました。

stdライブラリの型のように自動で出力できるものもありますが、基本的にはトレイトにより実装を強制されるメソッドの実装が必要です。

しかし、デバッグのときに毎回そのためのメソッドを実装するのは手間です。それを回避する方法も用意されています。deriveアトリビュートというものを使います。

// この構造体は`fmt::Display`、`fmt::Debug`のいずれによっても
// プリントすることができません。
struct UnPrintable(i32);

// `derive`アトリビュートは、
// この構造体を`fmt::Debug`でプリントするための実装を自動で提供します。
#[derive(Debug)]
struct DebugPrintable(i32);

Swiftのattributeという機能に少し似ていて、こちらはコンパイラに対して宣言や型の補足情報を伝えます。deriveアトリビュートも宣言前に記述しています。

Rustではアトリビュートを使って宣言を修飾することができます。

その他のアトリビュートについてはアトリビュートを参照してください。

その他

  • ファイル名に一つ以上の単語を使うならアンダースコアを使う(例:hello_world)
  • Rust 2018 Editionから外部モジュールをインポートする時にextern crate xxxxではなくいきなりモジュール名がかけるようになったuse rand::Rng;
  • インデントはスペース4つ。議論は収束していてフォーマッタはタブをオプションでサポートしている。議論はここLine and indent width · Issue #1 · rust-dev-tools/fmt-rfcs

まとめ

iOSアプリエンジニアの自分がRustを使える場面はそう多くないです。Rustはコンパイルエラー時の出力がすごく丁寧で、所有権などのRustの優れた言語機能を学びつつ、Rustのコンパイラにより良いコードを書けるよう教育してもらうぐらいのつもりでRustに興味を持ち勉強会に参加しました。

最終的にはCLIツールを作るぐらいを当面の目標にこの勉強会でRustの基礎を身に着けたいなと思っています。

これからも引き続き勉強会のレポートを残していきたいとおもいます。また、この記事はTRPLや公式のドキュメントやコンパイルの出力、実際に書いたコードなどを元に構成しています。まだまだ初心者なので誤りがあればコメントやTwitterなどでご指摘いただけるとありがたいです。