満を持して始める Rust

こんにちは、 @kenkoooo です。2月に開催されたDevelopers Summit (デベロッパーズサミット, デブサミ) のセッション「満を持して始める Rust」で発表した内容をブログでお届けします。セッションの内容を再現しているので、色んな話題を詰め込んだ忙しい内容になってしまいましたがご容赦ください。

Rust とは

Rust とは、Mozilla が初期から公式プロジェクトとして開発を進めてきたプログラミング言語で、コンパイラがメモリ安全性を保証するという特徴があります。バージョン 1.0 のリリースから8年ほど経ち、広く使われるようになってきました。

乗るしかない このビッグウェーブに

とはいえ、Rust は難しいという噂もよく聞きます。しかし、個人的には、よく言われる Rust の難しさの大部分が、プログラミングそのものの難しさを明らかにしているだけだと考えています。

そもそもプログラミングが難しい

次のような Python のコードを考えてみます。

class Foo:
   def __init__(self, a):
       self.a = a
   def foo(self):
       self.a.append("Foo")

class Bar:
   def __init__(self, a):
       self.a = a
   def bar(self):
       self.a.append("Bar")

a = []
foo = Foo(a)
bar = Bar(a)

foo.foo()  # a = ["Foo"]
bar.bar()  # a = ["Foo", "Bar"]

このコードは Foo から見ると、いつの間にかメンバ変数 a"Bar" が追加されているように見え、 Bar から見ると、いつの間にかメンバ変数 a"Foo" が追加されているように見えます。同様のコードをナイーブに Rust で実装するとコンパイルが通りませんが、Rust が難しいというよりも、Rust コンパイラによってリソースが曖昧に管理されている実態が明らかになっているだけと言えます。このように、Rust の難しさとされる部分は、Rust そのものの難しさではなく、Rust コンパイラによってプログラミングの難しさが表面化しているものと言えます。

コンパイラが強い

先ほど見たとおり、Rust ではコンパイラがコンパイル時にリソース管理についてチェックしてくれます。これによって、実行時エラーを踏む危険を減らすことができ、より安心してコードを書くことができます。

これらのリソース管理を、Rust では所有権という概念を導入することによって実現しています。他のプログラミング言語ではあまり見ない概念ですが、コンパイラがエラーメッセージを通して教えてくれるので、書きながら学ぶことができます。

オススメ機能: Result

Rust には、普段遣いのプログラミング言語として便利に使える機能がたくさんありますが、個人的に好きな Result について紹介します。

次のような Java のコードを考えてみます。

String readFile() throws IOException {
    ...
}

いかにも例外を投げそうな関数ですし、この関数を使う側で try-catch するのを忘れずに済みそうです。

次のような関数はどうでしょうか?

String readFile() {
    ...
}

インターフェイスからは例外を投げなそうなので、 try-catch する必要はなさそうに思えます。しかし、次のように、実行時エラーを投げるかもしれません。

String readFile() {
    ...
    throw new RuntimeException("IO Error");
}

このように、インターフェイスから例外を投げるかどうかが分からない場合、関数の中身を読むしかありませんし、最悪本番でクラッシュすることもありえます。

Rust ではこのような関数は Result を返します。

fn read_file() -> Result<String, IOError> {
   ...
}

このように、エラーを返しうる関数がインターフェイスから分かるようになっているため、エラーハンドリングを忘れることはありません。

次に、以下のような Go のコードを考えてみます。

func ReadFile() (string, error) {
    ...
}

このコードはインターフェイスからエラーを返しうることが分かります。この関数のエラーは次のようにハンドルします。

result, err := ReadFile()
if err != nil {
    // ...
}

Goでは「エラーだった時は後続の処理はせずに早期 return する」処理として、次のようなコードが頻出します。

if err != nil {
    return nil, err
}

Rust では、同様の早期 return を次のように ? の1文字で実現することができます。

let result = read_file()?;

このように、 Result でエラーハンドリングを漏らさないようにしつつ、 ? で簡単に処理できるようにすることで、エラーを含む処理の記述が圧倒的にやりやすく、安全なコードを書きやすくなっているのではないかと個人的には思っています。

すぐ作るウェブ API サーバー

早速 Rust でウェブ API サーバーを作ってみましょう。

Rust ツールチェインがインストールされた環境で、次のコマンドでプロジェクトを作成します。

cargo new demo

すると、 demo ディレクトリが作成され、中に Cargo.tomlsrc/main.rs が作成されます。

demo/
├── Cargo.toml
└── src
    └── main.rs

demo ディレクトリに移動し、次のコマンドでウェブフレームワークの actix-web と JSON ライブラリの serde_json をインストールします。

cargo add actix-web serde_json

そして、actix-web の README から、以下のサンプルコードをコピペし、 main.rs に貼り付けます。

use actix_web::{get, web, App, HttpServer, Responder};

#[get("/hello/{name}")]
async fn greet(name: web::Path<String>) -> impl Responder {
    format!("Hello {name}!")
}

#[actix_web::main] // or #[tokio::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(greet)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

これを実行してみましょう。VS Code では rust-analyzer の拡張機能を入れておけば、次の画像のように main を実行するための Run ボタンが表示されているはずなので、これを押します。

http://localhost:8080/hello/rust にアクセスするとメッセージが表示されるはずです。自分の名前を入れて反応を楽しみましょう。

API サーバーといえば JSON を返してほしいですよね。先ほどのコードの greet 関数を次のように書き換えて、プロセスを再度実行してみましょう。

#[get("/hello/{name}")]
async fn greet(name: web::Path<String>) -> impl Responder {
    web::Json(serde_json::json!({
        "name": name.to_string()
    }))
}

せっかくなのでデータベースと接続したいですが、SQL を用意したりするのは大変なので、いったん HashMap をなんちゃってデータベースとして使うことにします。次のように main を書き換え、 HashMap をウェブサーバーに渡します。キーがユーザー名、値がメッセージのつもりです。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let mut map = HashMap::new();
    map.insert("rust".to_string(), "hello".to_string());

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(map.clone()))
            .service(greet)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

ウェブサーバーに渡した HashMap はエンドポイントで使うことができます。 greet の引数に map: web::Data<HashMap<String, String>> を追加します。

#[get("/hello/{name}")]
async fn greet(name: web::Path<String>, map: web::Data<HashMap<String, String>>) -> impl Responder {
    web::Json(serde_json::json!({
        "name": name.to_string()
    }))
}

これで出力に変化はありませんが、いったん再実行してコンパイルが通ることを確認します。

このデータベースに存在しているユーザーには、あらかじめ登録されているメッセージを返し、そうでないユーザーにはエラーメッセージを返すようにします。 greet を次のように書き換えます。

#[get("/hello/{name}")]
async fn greet(name: web::Path<String>, map: web::Data<HashMap<String, String>>) -> impl Responder {
    match map.get(name.as_ref()) {
        Some(message) => web::Json(serde_json::json!({ "message": message })),
        None => web::Json(serde_json::json!({ "message": "not found!" })),
    }
}

登録されているユーザーを増やして、反応を楽しみましょう。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let mut map = HashMap::new();
    map.insert("rust".to_string(), "thank you".to_string());
    map.insert("kenkoooo".to_string(), "hello".to_string());

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(map.clone()))
            .service(greet)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Rust 導入事例

estie の開発事情と Rust 選定理由

estie は商業用不動産市場の課題を解決することを目指していますが、この課題は深く複雑で、単一プロダクトでは解決しきれないと考えています。

そこで、社内では次々に新しいプロダクトが立ち上がっています。プロダクト間のコミュニケーションは API 越しに行われるため、内部でどのようなプログラミング言語が使われているかはお互いに意識しておらず、プロダクトごとに開発チームで決めたプログラミング言語が使われています。

Rust はいくつかのプロダクトで使われていますが、以下のような選定理由があります。

  • 動き続けるウェブサービスで使われているので、本番環境で例外を漏らしたり null を踏んだりするのを避けたく、エラー処理・null 処理に強いほうが良い。
  • チームでスピード感をもって開発するため、コードの品質をある程度自動的に担保できるように、コンパイラが厳格であるほうが良い。
  • 開発支援ツールが豊富にある方が良い。

開発者事情

estieで Rust を使っている開発者の大半が、estie で初めて Rust に触れています。今のところ Rust 研修のようなものは実施しておらず、稼働中のサービスのコードを実際に動かしながら学んでいくということにしています。ハマるところは人それぞれなので、適宜ペアプロなどしつつ、コードレビューなどでフォローしています。

最後に、社内の Rust 入門者の皆様の声をお届けします。

  • ”?” などは見慣れないため、初見では驚いてしまうが、目が慣れたら分かるようになった。
  • Result / Option は慣れるまでは難しく感じた。関数型言語の経験があると違うかも。
  • オブジェクト指向の考え方は Rust でも通用すると感じた。「継承」ができないくらいだが、継承も推奨されていないので、気にならない。
  • 難しい印象を持っていたが、直感的に書けるので、普通に使う分には難しくない。また、色々な書き方ができるので、いったん動くものを作ることはでき、どうやっても全然動かないということにはならない。

おわりに

Rust を採用したことによって、コンパイラによってコードの品質が自動的に保たれ、開発効率が上がっていると感じています。一方で、estie は商業用不動産市場全体の DX を目指しており、まだまだ実装したいものがたくさんあります。ぜひ入社して、実装していただけませんか?ちなみに、他に Node.js を使うチームや、Ruby on Rails を使うチームもあるので、Rust を使いたくない場合でも大丈夫です。

いきなり入社するのは色々大変だと思うので、まずは話だけでも聞きに来てください。

hrmos.co

© 2019- estie, inc.