はじめに
普段のお仕事では JavaJava してる筆者です。
Rust が旬らしいので、試しに簡単な GraphQL サーバーを建ててみました。
基本的な機能はライブラリに頼りましたが、Rust の文法を一通り確認するにはちょうどいいように感じました。
GraphQL サーバー建てたいだけなら他の言語の方がいい。
最終的なコードはこちら。
この記事の内容
-
- 開発環境構築(Docker)
-
- 開発中に引っかかった文法
- 使ってみた所感
開発環境構築(Docker)
ローカル死んでも汚したくないマンなので、Docker を使います。ちなみに筆者は MacBook Pro をそのまま使用しています。
普通にローカルのディレクトリをマウントすると、ビルドが死ぬほど遅くなります。ビルド時のファイル同期処理がボトルネックになっているようです。
Rust のビルドディレクトリは、デフォルト設定だとルートディレクトリ直下に置かれます。噂によるとDocker for Mac の場合、マウントしたファイルの同期がかなり遅いんだとか。
というわけで、こちらの記事を参考に Dockerfile を作成しましょう。
FROM rust:1.41-buster
WORKDIR /usr/src/app
RUN USER=root cargo init --bin
ENV CARGO_TARGET_DIR=/tmp/target
COPY ./Cargo.toml Cargo.toml
COPY ./Cargo.lock Cargo.lock
RUN cargo build \
&& rm src/main.rs
EXPOSE 8000
CARGO_TARGET_DIRでビルドディレクトリを指定しています。ファイル同期を行わないディレクトリを指定してあげましょう。これだけで(対策前に比べると)かなりサクサクになります。
他にもいろいろ書いていますが、だいたい初回ビルドを行うためのものです。初回ビルドは依存ライブラリの影響でどうしても重くなるので、先にやっておこうということですね。
開発中に引っかかった文法
TRPLをしっかり読んでいれば引っかかることはないでしょう。
サイズ不明なトレイトはスマートポインタに格納する
TRPL のこのページに書かれている内容です。
Rust では一部の文脈において、対象のサイズをコンパイル時に特定できている必要があります。
具体的に言うと、以下の記述は怒られます。
trait Shape {
fn area(&self) -> f32;
}
fn print_area(s: dyn Shape) { // `s` doesn't have a size known at compile-time
println!("{}", s.area());
}
Shapeはトレイト(Java でいうインターフェースのようなもの)なので、定義した段階ではそのサイズは不明です。底辺と高さをフィールドに持つ三角形かもしれませんし、半径のみを持つ円形かもしれません。
そのため、コンパイル時にはサイズが判断できず、エラーとなります。
というわけで、Boxを使いましょう。
trait Shape {
fn area(&self) -> f32;
}
fn print_area(s: Box<dyn Shape>) {
println!("{}", s.area());
}
Box に格納されたデータはヒープに配置されます。Box 自身はシンプルなスマートポインタなので、格納するデータに寄らず、サイズは常に一定です。
また、Box にはDerefトレイトが実装されているため、普通の参照と変わらない感覚で操作することができます。
このように、サイズ不定なトレイトはスマートポインタに格納することで疑似的にサイズ特定可能になります。
Box 以外にも Rc や Arc等があるため、そちらもチェックしておくといいでしょう。
?演算子
TRPL のこのページに書かれている内容です。
Rust にはResult 型があります。他の言語では Either とか呼ばれているやつですね。
失敗する可能性のある関数を定義するとき、正常なら戻り値を格納したOkを、異常ならエラーなどを格納したErrを返却するようにしてあげる、等の使い方をします。
呼び出し側は Ok のときと Err のときの 2 通りを想定してあげなければなりません。
例えば、以下のようなコードがあるとします。
// a / b
fn div(a: f32, b: f32) -> Result<f32, String> {
match b != 0.0 {
true => Ok(a / b),
false => Err(String::from("Divided by zero.")), // ゼロ除算はエラー
}
}
// (a / b) + c
fn calculate(a: f32, b: f32, c: f32) -> Result<f32, String> {
let rd = div(a, b);
match rd {
Ok(d) => Ok(d + c),
Err(e) => Err(e), // ゼロ除算によるエラーはそのまま呼び出し元に渡す
}
}
divは失敗するかもしれない関数であるため、結果に応じて処理を分岐させる必要があります。
必要な処理ではありますが、冗長に感じますね。
ここで?演算子を使ってあげましょう。calculateに注目です。
// a / b
fn div(a: f32, b: f32) -> Result<f32, String> {
match b != 0.0 {
true => Ok(a / b),
false => Err(String::from("Divided by zero.")), // ゼロ除算はエラー
}
}
// (a / b) + c
fn calculate(a: f32, b: f32, c: f32) -> Result<f32, String> {
let d = div(a, b)?; // エラーの場合はここで処理を終了し、呼び出し元に渡す
Ok(d + c)
}
一気に簡単になりましたね。
?演算子は Result 型が返り値である関数を呼び出す際に使えます。
呼び出した関数の戻り値がエラーだった場合、呼び出し元に返してくれます。正常だった場合はResult型を取っ払ってくれます。
エラーのもつ型と返却すべき型が異なる場合は、fromトレイトが実装されている場合に限り自動で変換してくれます。
この通り、失敗するかもしれない関数を連続して呼び出すときには非常に便利です。
エラー時に内包する型はstd::error:Errorトレイトである場合が多いです。
トレイトのままでは扱いにくいので Box に格納します(Result<Any, Box>)。が、Error -> Boxはfromトレイトが実装されているため自動で行われます。超便利。
使ってみた所感
コンパイラが厳格
Rust の開発ではものすごくコンパイラに怒られます。やれ所有権がおかしいだのサイズがわからんだのdynつけ忘れてるだの……
一方で、実行時エラーはあまり起きませんでした(無いわけではない)。
コンパイラが厳格な分、実行がスムーズにいくのはありがたいですね。筆者のようにコンパイラと殴りあうのが楽しいエンジニアには向いているかと思います。
基本的なツールがそろっている
Option型やResult型が基本ライブラリとして実装されており、サポートする機能も備わっています。
また、ユニットテストについても追加のライブラリ無しに実行できるため、いちいちライブラリの選定や管理などに頭を使わずに済みます。
必要なものは最初から入っているのがとても便利に感じます。
マクロはさっぱりわからない
Rust にはマクロの機能があります。今回利用したactix-webやjuniperなどのライブラリでも利用されていますね。
このマクロですが、中で何をやっているのか非常にわかりにくく感じました。
例えばjuniper::graphql_objectマクロを見てみましょう。
(
@generate,
meta = {
lifetimes = [$($lifetimes:tt,)*],
name = $name: ty,
ctx = $ctx: ty,
main_self = $main_self: ident,
outname = {$($outname: tt)*},
scalar = {$($scalar:tt)*},
$(description = $desciption: expr,)*
$(additional = {
$(interfaces = [$($interface:ty,)*],)*
},)*
},
items = [$({
name = $fn_name: ident,
body = $body: block,
return_ty = $return_ty: ty,
args = [$({
arg_name = $arg_name : ident,
arg_ty = $arg_ty: ty,
$(arg_default = $arg_default: expr,)*
$(arg_description = $arg_description: expr,)*
$(arg_docstring = $arg_docstring: expr,)*
},)*],
$(decs = $fn_description: expr,)*
$(docstring = $docstring: expr,)*
$(deprecated = $deprecated: expr,)*
$(executor_var = $executor: ident,)*
},)*],
) => { … };
(
@parse_interfaces,
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = {
lifetimes = [$($lifetime:tt,)*],
name = $name:ty,
ctx = $ctxt: ty,
main_self = $mainself: ident,
outname = {$($outname:tt)*},
scalar = {$($scalar:tt)*},
$(description = $desciption: tt,)*
$(additional = {
$(interfaces = [$($_interface:ty,)*],)*
},)*
},
items = [$({$($items: tt)*},)*],
rest = [$($interface: ty),+] $($rest:tt)*
) => { … };
(
@parse_interfaces,
success_callback = $success_callback: ident,
additional_parser = {$($additional:tt)*},
meta = { $($meta:tt)* },
items = [$({$($items: tt)*},)*],
rest = interfaces: $($rest:tt)*
) => { … };
(
@parse,
meta = {$($meta:tt)*},
rest = $($rest:tt)*
) => { … };
(@$($stuff:tt)*) => { … };
(
$($rest:tt)*
) => { … };
}
えっと……何この……なに?
筆者のマクロ知識がひよこ未満なのが主原因ですが、難解ですね。
とはいえ、vec!をはじめとして上手くマクロを使えれば便利なことは間違いないようです。
おわりに
Rust を触ったことで、ポインタやスタック・ヒープについて意識する機会ができ、勉強になりました。
また、コンパイラと殴り合うのは楽しいと再認識できました。たまに心折れるけど。
参考資料
The Rust Programming Language
[Rust] docker コンテナにマウントしたディレクトリで build すると遅かった話し
DX を大幅に低下させる Docker for Mac を捨てて Mac 最速の Docker 環境を手に入れる
Rust で DI(依存注入)する
Rust の所有権まわりの基礎まとめ
Rust チートシート
Rust のエラーまわりの変遷
Rust で Option 値や Result 値を上手に扱う
Rust の?演算子
Rust の Arc を読む(1): Arc/Rc の基本
Rust のマクロを覚える
実践クリーンアーキテクチャ