エウレカ社にてRustのハンズオンを実施しました。
コード全体は下記で確認できます。 https://github.com/yuk1ty/rust-basic-handson
2021/7/16 @ 株式会社エウレカRust ハンズオン
View Slide
江間さんご招待ありがとうございます
Hello, I’m...Yuki ToyodaSoftware Engineer @ CyberAgent DynalystNext Experts in Rust @ CyberAgent共著『実践Rustプログラミング入門』Rust.Tokyo, RustFest Global オーガナイザContributor to rust-lang/rust, servo, etc@helloyuki_
Next Experts についてNext Experts 3つの役割特定領域への貢献意欲や一定の実績を有し、将来的なDeveloper Expertsを目指すエンジニアのための活動支援制度。サイバーエージェントにはDeveloper Expertsと呼ばれる、特定の専門領域における社内・社外活動の支援制度が存在します。各専門領域における高い専門性を身につける対外活動を通して、各専門領域の発展に貢献管轄や事業部の垣根を越え、担当領域のサポート
『実践Rustプログラミング入門』Rustは、C/C++の代わりとなる最新の爆速言語として注目されています。「とにかく実行速度が速い」「モダンな言語機能が一通り入っている」「OSからWebアプリケーションまで幅広く実装できる」「ツール群がとても充実している」「安全性が強力に担保されている」など、数多くの魅力があります。本書は、JavaやPythonなど他の言語に習熟しているエンジニアを対象に、Rustの独特な仕様と開発ノウハウをわかりやすく解説した入門書です。
Rust.Tokyo, RustFest Global オーガナイザ日本初の Rust カンファレンス「Rust.Tokyo」や、グローバルな Rust カンファレンス「RustFest Global」のオーガナイザも務めています。2021年9月18日に Rust.Tokyoは開催予定です。CfP も受付中ですので、ご興味がある方はぜひ、ご応募ください。RustFest Global ミーティングの様子Rust.Tokyo 2021 のロゴ
目次Rust とは基本的な文法cat コマンドの実装grep コマンドの実装
Rust とは
Rust とは2015年にリリースされたプログラミング言語。得意な領域はシステムプログラミング。速度と安全性を両立した新しい言語。
2015年にリリースされたプログラミング言語オープンソースで開発が進められている。ブラウジングエンジン Servo のフィードバックを受けて成長。Stack Overflow 愛され言語ランキング5年連続1位
得意な領域はシステムプログラミングOS/コンパイラ/ランタイム・VM/ブラウザなど、低レイヤーの操作が必要なソフトウェアの実装に最適化されている ️Redox OS Servo Deno
ただ…Web などでも使える文法が非常に表現力豊かで書き心地もよいことから、低レイヤーだけでなくWeb バックエンドや WebAssembly ベースのアプリケーションなどのレイヤーの高い世界でも活用が始まっている。Wasmer (WebAssembly Runtime)別所で以前登壇した資料
速度と安全性を両立した新しい言語安全性面平均してGo/Javaより速く、C/C++並のスピードが出る。ゼロコスト抽象化により、あらゆるコードを静的に解決する。GC やランタイムはなく、機械語を直接生成する。DX と速度がトレードオフにならないエルゴノミックな文法。速度面GC はないが、メモリ安全である。safe コードでは、実行時に未定義動作が発生しない保証がされる。メモリ確保と解放をコンパイラが自動で検査する。
速度と安全性を両立した新しい言語安全性面平均してGo/Javaより速く、C/C++並のスピードが出る。ゼロコスト抽象化により、あらゆるコードを静的に解決する。GC やランタイムはなく、機械語を直接生成する。DX と速度がトレードオフにならないエルゴノミックな文法。速度面GC はないが、メモリ安全である。safe コードでは、実行時に未定義動作が発生しない保証がされる。メモリ確保と解放をコンパイラが自動で検査する。速度を出しつつ安全性を追求し、従来存在したトレードオフを打破した言語
本日の内容をより詳しく知るためにRust をはじめるための資料集https://blog-dry.com/entry/2021/01/23/141936
基本的な文法
ツールの準備cargo をインストール。これだけ。https://www.rust-lang.org/ja/tools/install
エディタはどうする?今日は VSCode を使って作業を進めます。rust-analyzer という Extension を入れておくと便利です。もちろんお好きなエディタでも、作業自体はできます。
今日のコード全体GitHub 上にすべて公開しています。もし途中で詰まったり、コピペしたくなったらご覧ください。https://github.com/yuk1ty/rust-basic-handson
まずは Hello, World してみましょう
まずは Hello, World してみましょう生成されたディレクトリの中に main.rs というファイルがあります。中身を見てみましょう。下記が Rust での Hello, World コードです。
まずは Hello, World してみましょうfn main() は関数を宣言しています。Rust では関数の定義は fn キーワードで行うことができます。
まずは Hello, World してみましょうprintln! で標準出力します。 は であることを意味します。マクロとは、Rust のプログラムをプログラミングする仕組みのことです。他の言語ではメタプログラミングとも呼ぶ機能のことです。`!` マクロ
実行してみましょう
変数宣言をしてみましょうlet というキーワードで変数を束縛することができます。になります。とすると、 な変数を作ることができます。デフォルトではイミュータブルlet mut ミュータブル
変数宣言をしてみましょうlet で変数宣言をできます。println! 内の {} は変数を文字列内に代入するプレースホルダです。Rust は型推論が強力なので、型注釈はほぼなしで実装できます。
値を再代入してみましょう多くのプログラミング言語では、下記はコンパイルが通るかも知れません。しかし、Rust ではコンパイルが通りません。変数はデフォルトでイミュータブルなので、下記はコンパイルエラーです。
Rust のコンパイルエラーはとても親切先ほどのコードをコンパイルすると、下記のようにエラーが出ます。指示通りに直すことで、多くのケースではコンパイルを通せます。
値を再代入してみましょうlet mut に変更するとその変数はミュータブルになります。ミュータブルな変数では再代入や破壊的な操作が可能です。
変数宣言まとめlet というキーワードで変数を束縛することができました。変数は、デフォルトではイミュータブルになります。let mut とすると、ミュータブルな変数にできました。
よく見るプリミティブ型 (※これ以外にもあります。詳しくはドキュメントをご覧ください)i8, i16, i32, i64u8, u16, u32, u64f32, f64isize / usizebool&str, String符号付き整数型。浮動小数点数型。CPU アーキテクチャごとにビット分のサイズを取る整数型。isize は符号付き。ブール値。true または false をとる。文字列を扱う際によく利用される型。後述。符号なし整数型。
文字列型今日は String と &str を紹介します。よく質問を受けるので、先に軽く紹介してしまいます。文字列型はこれ以外にもあります。
String と &strString&strヒープメモリにアロケートされる。リサイズ可能な UTF-8 のテキストを保持するバッファ。いわゆる 。なので、String と違ってリサイズ等はできない。「スライス」read-only
String のメモリモデルstackheap t u t o r i a l10 8capacitycapacitylengthbufferlength※ stack: いわゆるスタックメモリを指す。ローカル変数などが保存される領域。※ heap: いわゆるヒープメモリを指す。動的に確保される領域で、C では malloc によって確保される。
&str のメモリモデルstackpreallocatedread-onlymemoryt u t o r i a l8lengthbufferlength※ preallocated read-only memory: 機械語生成時にすでに確保済みのメモリ領域を指す。
String, &str を使うにはダブルクォーテーションで囲うと文字列を扱うことができますが、その際の型は &str 型になります。たとえば、to_string() 関数を呼び出すと、String 型になります。
FizzBuzz を通じて制御構文や関数を学ぶif 式を使った簡単な制御構文を学びます。for 式を使ったループを学びます。関数への切り出し方を学びます。関数型プログラミング的なアプローチを学びます。
if 式を使って FizzBuzz を作ってみる
if 式を使って FizzBuzz を作ってみる他の言語と同様に if キーワードが存在します。if は なので、結果を変数に束縛できます。セミコロンを文末につけると式になり、ない場合は文として解釈が行われます。if 式がある関係で、三項演算子は Rust にはありません。式
for 式を使って0〜99の数字をイテレートする
for 式を使って0〜99の数字をイテレートする0..100 と書くと、0以上100未満の範囲を示すオブジェクト (Range) を生成できます。num に0〜99の値が1つずつ流れ込みます。Range は の性質をもちます。for はイテレータを回す糖衣構文になっています。イテレータ
関数に切り出す
関数に切り出すfn 関数セミコロンを書かないと return 扱いと宣言すると を宣言できます。関数名は snake_case です。引数は「仮引数名: 型名」で定義できます。返りの型は -> のあとに書きます。return は基本は必要なく、になります。
関数型的なアプローチこれまでは for を用いた手続き型的なアプローチを見てきました。Rust は関数型プログラミングの手法をいくつかとりいれています。関数型的なアプローチを使った FizzBuzz を紹介します。
関数型的なアプローチ
関数型的なアプローチ0〜99のイテレータを作成し、その結果を fizzbuzz 関数に通します。結果生成される文字列を結合していき、ひとかたまりにまとめます。| ... | で囲まれた部分は と呼びます。他の言語ではラムダ式など。クロージャー
関数型的なアプローチわたしは日頃関数型プログラミング言語を使用しているので、こちらの方が宣言的でより馴染みがあり好きです。どちらのスタイルで書いたとしても、速度に差はほぼ出ません。※ ゼロコスト抽象化の恩恵によるものです。理論上はそうなります。ただ、ケースや最適化のされ方によっては差が出るものも多少あるかもしれません。
現時点で紹介していないもの、省いたものstruct、enum、所有権、借用については後述します。ジェネリクスやトレイト、ライフタイムなどは説明していません。詳しくは TRPL などのリファレンスを参照してください。※ TRPL = The Rust Programming Language。Rust を学習する際のバイブル的な本です。
cat コマンドを実装する
実践編: cat コマンドの実装基本文法を一通り学んだところで、実際に使えるツールを作りながら、Rust の書き心地を学びます。パターンマッチングや enum を紹介します。
最終目標
cat, grep で使用するプロジェクトを新規作成する
main.rs 自身を表示するプログラムを書く
main.rs 自身を表示するプログラムを書くread_to_string 関数は、パスの内容を読み込んで String 型にして返します。は他の言語だと import のような記法で、 を使用することを意味します。は という文法です。Ok や Err は Result 型という です。use モジュールmatch 「パターンマッチ」enum のバリアント
パターンマッチ他の言語でいう です。Rust では多くの型がパターンマッチに対応しています。いくつか例を示します。switch 文をもう少し強力にしたもの
パターンマッチの例数値型はパターンマッチングの対象になります。1, 2, 3 の場合にそれぞれの文字を取り出す、`_` はそれ以外のパターンの場合という意味になります。
パターンマッチの例文字列もパターンマッチングの対象にできます。`start...end` は「startからendの範囲で」という意味になります。
Rust のエラーハンドリング: Result 型Rust ではエラーハンドリングを Result 型で行います。成功なら Ok 、失敗なら Err が返ります。パターンマッチなどを使って中身を取り出します。
Result 型のパターンマッチングの仕方Ok(content) で、content という変数に内容をもちます。Err(reason) の場合は、reason という変数にエラー内容が入ります。それらを後続の処理で使用することができます。
? キーワード常にパターンマッチングが必要なわけではなく、ことができるようになっています。伝播先での値の取り出し時には、必ずエラーハンドリングが求められます。`?` というキーワードでエラーを伝播させる
ファイルパスを実行時引数から渡せるようにする
今回記述が増えた内容についてstd::env::args は関数で、実行時引数の取り出しに利用できます。nth で実行時引数を取り出します。Some, None は Option 型で、値がない可能性があることを示します。
値がない可能性があることを示す: Option 型他の言語で言う です。Some なら値があり、None なら値がなかったことになります。パターンマッチなどを使って中身を取り出します。null や nil が入る場所に使われる型
Option 型のパターンマッチングの仕方Some(path) で、path という変数に内容を持ちます。None の場合は値がないので、何ももちません。
if letOption の場合、よく「値があるときだけ特定の処理をしたい」ケースに出会います。その際は、None の場合は不要になります。パターンマッチングだと冗長です。代わりに if let という構文を利用しているコードをよく目にします。
grep コマンドを実装する
実践編: grep コマンドの実装cat コマンドを改変すると、grep コマンドを作れます。構造体や所有権を勉強しつつ、ライブラリをいくつか使って grep を拡張してみます
指定した文字列がある行を検索し出力する
指定した文字列がある行を検索し出力する先ほどの run_cat 関数を run に変更しています。grep 関数を作り、中で指定した文字パターンに一致するかチェックしています。.lines() 関数により、1行1行イテレータを回すことができるようになっています。
指定した文字列がある行を検索し出力する今回は1つ目の引数で検索対象を、2つ目の引数でファイルパスを指定します。(pattern, path) は と呼ばれる文法です。パターンマッチして使用しています。両方にきちんと引数の指定がない場合はエラーメッセージを出すようにしています。タプル
ここまでの実装cat 用の実装に検索実装を追加して、一致した文字列があった際に標準出力するように修正しました。実行時引数で検索したい文字列を受け取れるようにしました。
grep に使用する引数を構造体にまとめる実行時引数として渡されるパラメータが増えてきました。データをひとまとまりにして新しい型して定義しましょう。構造体(struct)の使い方を見ていきます。
構造体を定義するstruct キーワードを使って を定義することができます。pattern や path はフィールドです。ちなみに snake_case です。「フィールド名: 型名」の順に定義します。構造体
構造体に対する実装を定義する 1impl 実装を定義できますキーワードを使って、構造体に対する 。今回はスタティックメソッドの new を定義し、それを後々 main で呼び出しします。インスタンスメソッドについては、これから解説します。
構造体に対する実装を定義する 2レシーバの を関数の第一引数につけると、いわゆるインスタンスメソッドになります。Python などと同じ形式だと思います。レシーバを経由して、構造体自身のフィールドにアクセスできます。self
作ったスタティックメソッドを呼び出してみる「構造体名::メソッド名」で呼び出しができます。インスタンスメソッドの場合は「変数名.メソッド名」です。
run 関数も修正するGrepArgs 型を引数で受け取りできるように、run 関数の仮引数も修正しましょう。また、構造体から必要な値を取り出しするように関数内も修正しましょう。
ここまでの実装GrepArgs という構造体を作り、それを run 関数に渡して処理を行うようにしてみました。
CLI ツールっぽくするstructopt というクレート(ライブラリ)を使います。CLI ツールのように help コマンド等を生やすことができます。便利な機能をいくつか利用して、CLI ツール化しましょう。
クレートを追加するCargo.toml の [dependencies] に structopt に対する依存を追加します。今回はバージョン 0.3.21 で使用します。追加したら、一度 cargo build などを回しましょう。
structopt の設定を行う#[derive(StructOpt)] は StructOpt トレイトを という意味です。#[structopt(...)] は、今回は説明しませんが と呼ばれるマクロです。継承する「手続きマクロ」※ はこのハンズオンでは紹介できませんが、他の言語で言うインタフェースのようなものです。トレイト
structopt で実行時引数をパースするfrom_args という関数を使うと、実行時引数をパースしてくれます。これを main 関数内で呼び出すようにします。std::env::args を使用していた箇所は、軒並み from_args で置き換え可能です。
ここまでの実装GrepArgs にいた new 関数は、from_args 関数を使用することにより不要になったので、削除しました。
help コマンドが生えてくるこの時点で help コマンドも同時に生成されています。cargo run -- --help と入力し、help が生成されていることを確認しましょう。
grep 関数の引数を GrepArgs を使うように修正するgrep の引数を GrepArgs を使用するように修正します。さらに、line.contains 関数内で使用していた pattern も state から呼び出しします。
run 関数の引数を GrepArgs を使うように修正するrun 関数でも同様に GrepArgs を使用するように引数を修正します。grep 関数に state を渡して grep 関数内でも使えるようにします。
おや…?コンパイルエラーがコンパイルエラーが発生するようになりました。これはなんでしょうか?
所有権と借用このコンパイルエラーは Rust の にまつわるものです。という機能を使うとこのエラーは出なくなります。Rust の所有権を簡単に解説します。所有権借用
所有権とはRust には で行われます。関数やブロックの終了タイミングでリソースの解放が走ります。Rust には値の所有者が必ず1つだけ存在します。所有者が移動することを と呼びます。GC がなく、リソースの解放は自動ムーブ
所有権の移動を見てみる
所有権の移動を見てみる11行目で束縛された user が、12行目に呼び出されている関数にムーブされました。13行目の println! マクロ内で user の値を使おうとしても、すでにムーブ済みです。
所有権の移動を見てみるHeapStack
所有権の移動を見てみるHeapStackMain Frame
所有権の移動を見てみるnameUser:newHeapStackMain Frameuser“user_a”
所有権の移動を見てみるHeapStacknameMain Frameuser“user_a”
所有権の移動を見てみるsome_action_to_userHeapStacknameMain Frameuser“user_a”
所有権の移動を見てみるtmpsome_action_to_userHeapStacknameMain Frameuser“user_a”
所有権の移動を見てみるsome_action_to_userHeapStackMain Frame
所有権を貸し出しする所有権は貸し出しすることができます。これをと呼びます。変数に & をつけることで、その変数の値を貸し出しすることができます。下記はコンパイルが通るようになります。借用
借用の考え方を使って、先ほどのコードを直してみましょうgrep 関数の GrepArgs の受け取りを参照にします。
借用の考え方を使って、先ほどのコードを直してみましょうrun 関数の state.path 部分と、grep に渡す state 引数は借用するようにしましょう。
ここまでの実装run と grep の引数を調整しました。借用の考え方を使って、関数を越えてもオブジェクトが解放されないように実装しました。
複数ファイル扱えるようにするgrep は複数のファイルにかけることができますよね。複数ファイルにかけられるようにコードを修正します。
複数ファイル扱えるようにするpath を (サイズが可変の配列; ベクタ) に変更します。複数ファイルを検索できる機能が裏で付与されます。Vec
各ファイルをイテレートして読み込みを行うiter() を使うと、ベクタ内をイテレートすることができるようになります。file に各ファイルのパスが取り出され、それを読み込んでいきます。
grep 関数を修正して、ファイル名も出力できるようにするfile_name を grep に渡せるように修正しましょう。中で一致する文字が見つかった場合に、ファイル名も標準出力できるようにしましょう。
ここまでの実装path を複数受け取れるよう、Vec を使って修正しました。受け取った path は for 文でイテレートされ、中身が取り出されるようになりました。また、検索結果にファイル名を含められるよう、引数の渡し方を調整しました。
関数型的なアプローチで書き直してみるrun 関数は for_each 関数を使って書き換えることができます。
並列処理をできるようにするgrep の各ファイルの検索は できるはずです。rayon というクレートを使って、並列にイテレータを回します。並列化
rayon を追加するCargo.toml に rayon への依存を追加します。今回はバージョン 1.5.1 を利用します。
run 関数を書き換えるrayon の prelude をインポートします。そして、run 関数内で先ほど書いた iter()関数を、par_iter() 関数に置き換えるだけで並列化できます。
このハンズオンで紹介しなかったものジェネリクストレイトライフタイムスマートポインタ(Box, Rc など)マクロの実装
興味が出た方は…The Rust Programming Language をぜひご覧ください。Rust には、他にも魅力的な機能がたくさんあります。
参考資料『実践Rustプログラミング入門』初田直也他『Programming Rust』Blandy & Orendorff『The Rust Programming Language』https://doc.rust-jp.rs/book-ja/chikoski/rust-handsonhttps://github.com/chikoski/rust-handson