Rustってどんな言語?Todoアプリ作成で入門してみよう

2022.01.28

2022.02.02

Fabeee社員ブログ


はじめまして、昨年10月よりFabeeeに入社したワカと申します!

文系未経験からPHP〜pythonなどなど書きながらゆるめにバックエンドエンジニアとしてマサカリを避けながら生き延びておりました。しかし、この度fabeee入社後の業務として近頃話題の言語「Rust」を使用したお仕事をさせていただくことになりました。本記事ではRust初学者が4ヶ月ほど勉強したRustについての知識をご紹介できればと思います。

Rustとは?

Rustはメモリ安全性、安全な並行性を目指して設計されたマルチパラダイムのプログラミング言語です。C言語・C++に代わるシステムプログラミング言語として開発され、それらと同様な高速実行かつ、独自の設計でメモリ安全性を実現していることが大きな特徴です。

2006年よりMoziila社支援のもと開発されているOSSであり、同社開発のWebブラウザエンジンに使用されているほか、2020年の「好きなプログラミング言語」1位に選ばれるなど(Stack Overflowの調査)近年注目度の高まってる言語の一つです。
OSやブラウザ、組み込み系などの低レイヤー開発からwebシステムのバックエンド開発、加えてブラウザ上でバイナリコードを高速実行できるwebAssembly(WASM)の生成など様々な分野で活用することができます。

ちなみに非公式マスコットはカニのFerris(フェリス)くんです。Go言語のGopherくんといい最近の言語にはマスコットまでついてて嬉しいですね。

Rustの学習コストについて

ざっと解説したようにRustはC言語並みの高速実行&パフォーマンス+メモリ安全性(メモリリークなどが起こりにくい)、プラス注目度も高いと夢のような言語ですが、一方学習コストがとても高いことで知られています。

これは自分の所感ですが、どのような言語に今まで触れてきたか、によって難しいと感じるポイントが異なるのではないかと思いました。
上記したメモリ安全性を保つ仕組みとしてRustには「所有権」という概念が用いられており、これがメモリ管理の経験がないと非常に難解です。
(正直なところ自分は今もしっかりと説明できない部分が多く、いまも勉強中です・・・)
前提としてC/C++ではメモリ管理はここがツライよね→Rustでは仕組みとして組み込んでしまおう!といった言語仕様の背景があるため、自分のようなそもそものメモリ管理の勘所を掴んでいない人間にはかなり理解に時間がかかる側面があります。Rustはコンパイルの時点でメモリ安全なコードかを精査されるため、所有権を理解して記述しなければ、コンパイルがいつまでも通らない・・・といった事態になりがちです。

加えてRustはマルチパラダイム言語でFP(関数型プログラミング)・OOP(オブジェクト指向プログラミング)・手続き型言語など様々な形式で記述することができるため、構文もかなり豊富です。型クラス(Rustではトレイトと呼ばれます)、クロージャー記法、構造体、ジェネリクスなど他言語で用いられている概念や記法が多く盛り込まれています。
そのため「〇〇(他言語)でみたことある書き方だ」といった部分がかなり多く、そういった経験が豊富な場合スムーズに理解していくことができると思います。

参考:Rustに影響を与えた言語たち-Qiita

Rust自体の学習コストは確かに高めではありますが、ドキュメントは比較的充実しており有志による日本語訳版も豊富です。
徐々に技術書も出版されており、学習しやすい環境は整い始めているといった印象です。

[web]

[書籍]

todoアプリを作ってみよう

今回は基本のTODOアプリを作りながら、基本的なRustのプロジェクト作成〜実行までをたどっていきたいと思います。
詳細なRustの言語仕様・文法などについては割愛させていただきます。
(上記のRust日本語ドキュメントから「The Rust Programming Language 日本語版」が一番詳細かつ正確なのでおすすめです)

環境

実行環境に関しては以下のとおりです。
※Rust環境構築に関しては上述の公式サイトから進めてください

> rustc --version
rustc 1.57.0 (f1edd0429 2021-11-29)

まずはcargo new でプロジェクトの新規作成を実行します。

cargo new todo-app

出力されたCargo.tomlにクレート(ライブラリ群)の依存関係を追加していきます。
今回は、Warpというシンプルなwebサーバ用フレームワークを使用します。
warpはtokioという非同期処理ライブラリに依存しているため、その記述も追加します。
さらにjsonにて結果返却を行うため、jsonシリアライズなどを行うserde(serde_derive)も追加。

 [dependencies]
 tokio = { version = "1", features = ["full"] } 
 warp = "0.3"
 serde = "1.0"
 serde_derive = "1.0"

まずはWarpのチュートリアルに沿って、単純なレスポンスを返すWebサーバを作成してみます。
https://github.com/seanmonstar/warp
…/todo-app/src/main.rsをレポジトリのExampleの通り変更。

use warp::Filter;
#[tokio::main]
async fn main() {
 // GET /hello/warp => 200 OK with body "Hello, warp!"
 let hello = warp::path!("hello" / String) 
   .map(|name| format!("Hello, {}!", name));

 warp::serve(hello)
   .run(([127, 0, 0, 1], 3030))
   .await;
 }

大まかな処理内容について

  • warp::path!(“hello” / String) 部分でルーティング定義。パスからパラメーターをString型で取得、mapで受け取ったパラメーターを文字列として埋め込んでいます
  • warp::serve(hello)部にてサーバ起動、上述のルーティングを読み込んでリクエストを待ちます

下記コマンドでコードのコンパイルとコード実行が走ります。

cargo run

起動したサーバに対してcurlでリクエストを送ってみましょう。
下のようにレスポンスが返ってくるはずです。

> curl http://localhost:3030/hello/fabeee
  Hello, fabeee!

todo一覧表示の作成

先ほど紹介したwarpのドキュメント内にTodoアプリについて実装例が紹介されているため、
今回はその実装を参考にしながら進めていきます。
https://github.com/seanmonstar/warp/blob/master/examples/todos.rs

まずはmain()内で読み込む部品として以下3つのモジュールに処理を分割します。

  • filters 主にそれぞれのルーティングを定義する、リクエストからhandlersの各メソッドへ割り振る
  • handlers 実際の処理を記述、Controller的立場
  • models dbへの接続(今回は簡易的に保存するようにします、起動中のみ保持)、todo自体の構造定義等

各モジュールに処理を割り振り、todo一覧の取得を実装します。
(もちろん実際のプロジェクトでは上記モジュールは別ファイルに分割して読み込む形になります。本記事は簡略化のためmain.rsに全てまとめてしまってます)

#[tokio::main]
async fn main() {
    let db = models::init_todos(); //todoデータ読み込み
    let routes = filters::todos(db); //ルーティング+handlers

    warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}

//ルーティング定義
mod filters {
    use super::handlers;
    use super::models::Db;
    use warp::Filter;

    //すべてのルーティングを束ねて返却する
    pub fn todos(
        db: Db,
    ) -> impl Filter + Clone {
        todos_list(db.clone())
    }

    //GET /todos => 一覧でtodoリスト返却
    pub fn todos_list(
        db: Db,
    ) -> impl Filter + Clone {
        warp::path!("todos")
            .and(warp::get())
            .and(with_db(db))
            .and_then(handlers::list_todos)
    }
    //dbとの接続
    fn with_db(db: Db) -> impl Filter + Clone {
        warp::any().map(move || db.clone())
    }
}

mod handlers {
    use super::models::{Db, Todo};
    use std::convert::Infallible;

    //Todoリストを返却
    pub async fn list_todos(db: Db) -> Result {
        let todos = db.lock().await;
        let todos: Vec = todos.clone().into_iter().collect();

        Ok(warp::reply::json(&todos))
    }
}

mod models {
    // dbからToDOリストを取得する
    use serde_derive::{Deserialize, Serialize};
    use std::sync::Arc;
    use tokio::sync::Mutex;

    //Todo構造体、各フィールド定義を記述して置くことができます
    #[derive(Debug, Deserialize, Serialize, Clone)]
    pub struct Todo {
        pub id: u64,
        pub text: String,
        pub completed: bool,
    }
    //dbの型定義、Arcはスレッド間で共有できるオブジェクトを提供します
    pub type Db = Arc<Mutex<Vec<Todo>>>;

    //todoリストの初期化
    pub fn init_todos() -> Db {
        //テスト 初期表示確認用
        let test = Todo {
            id: 1,
            text: "TODOの内容について".into(),
            completed: false,
        };

        Arc::new(Mutex::new(Vec::from([test])))
    }
}

再びcargo runで起動し、todo一覧取得のリクエストを送ってみましょう。

> curl http://localhost:3030/todos
[{"id":1,"text":"TODOの内容について","completed":false}]

無事Todoの一覧が表示できました。

新規Todoの追加

つづいてTodoの新規追加を実装してみます。
filtersにはPOST時のルーティングを、handlersには受け取ったtodoをリストに追加する処理をそれぞれ追加します。

...

//ルーティング定義
mod filters {
    use super::handlers;
    use super::models::{Db, Todo};
    use warp::Filter;

    //すべてのルーティングを束ねて返却する
    pub fn todos(
        db: Db,
    ) -> impl Filter + Clone {
        //以下にルーティング定義を足していく
        todos_list(db.clone())
            .or(todos_create(db.clone()))
    }

    //GET /todos => 一覧でtodoリスト返却
    pub fn todos_list(
        db: Db,
    ) -> impl Filter + Clone {
        warp::path!("todos")
            .and(warp::get())
            .and(with_db(db))
            .and_then(handlers::list_todos)
    }
    //dbとの接続
    fn with_db(db: Db) -> impl Filter + Clone {
        warp::any().map(move || db.clone())
    }

    //POST /todos => 新規Todoの登録
    pub fn todos_create(
        db: Db,
    ) -> impl Filter + Clone {
        warp::path!("todos")
            .and(warp::post())
            .and(decode_json())
            .and(with_db(db))
            .and_then(handlers::create_todo)
    }

    //jsonの読み取り
    fn decode_json() -> impl Filter + Clone {
        warp::body::content_length_limit(1024 * 16).and(warp::body::json())
    }
}

mod handlers {
    use super::models::{Db, Todo};
    use std::convert::Infallible;
    use warp::http::StatusCode;

    //Todoリストを返却
    pub async fn list_todos(db: Db) -> Result {
        let todos = db.lock().await;
        let todos: Vec = todos.clone().into_iter().collect();

        Ok(warp::reply::json(&todos))
    }
    //Todoリストに新規追加
    pub async fn create_todo(create: Todo, db: Db) -> Result {
        let mut vec = db.lock().await;
        //idが重複していた場合弾く
        for todo in vec.iter() {
            if todo.id == create.id {
                return Ok(StatusCode::BAD_REQUEST);
            }
        }

        vec.push(create);
        Ok(StatusCode::CREATED)
    }
}

modelsやmain()の処理は特に変更なしで大丈夫です。
それでは再び動作確認を行ってみましょう。

> curl -X POST -H "Content-Type: application/json" -d '{"id":2, "text":"追加したTODOです", "completed": true}' -i http://localhost:3030/todos
HTTP/1.1 201 Created
content-length: 0
date: Wed, 26 Jan 2022 06:33:01 GMT

> curl http://localhost:3030/todos
[{"id":1,"text":"TODOの内容について","completed":false},{"id":2,"text":"追加したTODOです","completed":true}]

> curl -X POST -H "Content-Type: application/json" -d '{"id":2, "text":"追加したTODOです", "completed": true}' -i http://localhost:3030/todos
HTTP/1.1 400 Bad Request
content-length: 0
date: Wed, 26 Jan 2022 06:35:48 GMT

id=2のtodoが追加されること、再び同IDで登録した場合弾かれることが確認できました。
新規追加機能については以上です。

その他機能(Todoの削除&更新)についてはほぼ参考元の実装例と変化ないため、割愛させていただきます。
今回のサンプルコードは以下のリポジトリに公開しているのでご参考までに!
https://github.com/TomoyaWk/rust-todo

まとめ

今回はRustについてのご説明、サンプルとしてTodoアプリを作成してみましたが、いかがだったでしょうか?
Rustは確かに学習コストが高く、自分もまだまだ勉強中の身です・・・。ですが、かなりコンパイラも優秀、構文で豊富で開発体験としてはとてもすばらしいもの
があると思います。
将来性もあり、これから学習していくのにピッタリの言語ですので、ぜひ入門してみてください。

この記事の監修者:杉森由政

この記事の監修者:杉森由政

2018年2月Fabeee株式会社CTOに就任。現在は、新規事業開発部門の責任者を務めるともに、AIやブロックチェーンなどの先端技術を用いた研究開発も担当。また広島大学との連携研究においては、生体データを解析する人工知能および解析アルゴリズムの研究開発にも携わるなど、多岐に渡り活躍をしている。
>>プロフィール情報