Open9

『Webアプリ開発で学ぶRust言語入門』を読む

enpolioenpolio

HTTPサーバーを立ち上げる

axumというWebアプリケーションフレームワークを使ってHTTPサーバーを立ち上げる。

https://github.com/tokio-rs/axum

Node.js + Expressと同じように、以下のような流れでサーバーを立ち上げる。

  • ルーティングの設定
  • サーバーを立ち上げる(listenする)
use axum::{routing::{get}, Router};
use std::net::SocketAddr;

let app = Router::new().router("/", get(handler));
let addr = SocketAddr::from([0, 0, 0, 0], 3000);

axum.Server::bind(&addr)
    .serve(app.into_make_service())
    .await
    .unwrap();

#[tokio::main]という属性マクロを使うことで簡単にマルチスレッドランタイムを構築できる。

https://docs.rs/tokio/latest/tokio/attr.main.html

#[tokio::main]
async fn main() {
  ...
}
enpolioenpolio

テストを書く

ユニットテストをするとき、大体#[cfg(test)]という注釈を使う
#[cfg(test)]が付いている場合、cargo testが走ったときにだけコンパイルされ、プロダクションではコンパイルされない

#[cfg(test)]
mod tests {
    use super::*;
    ...
}

testsmodと宣言されている通り、モジュールである
しかし内部モジュールであるため、use super::*;で、外部モジュールで定義したものをテストでも使えるようにしている

enpolioenpolio

スレッドセーフ

複数のスレッドで処理を行う際、同じリソースを扱っても正しい結果が得られるようにしなくてはならない。
そのような状態をスレッドセーフと呼ぶ。

Rustでスレッドセーフな処理を行うには、std::sync::Arcを用いる。
Arcで包んだオブジェクトは複数から参照されてもスレッドセーフになる。

https://doc.rust-lang.org/std/sync/struct.Arc.html

本書ではTodoRepositoryForMemoryはデータベースの代わりを果たし複数から参照されうるので、Arcでラップしてあげる。
より正確に言うと、Arc<RwLock<>>でラップしている。RwLockは複数スレッドからのread及び1つのスレッドからのwriteを許可する(つまり複数から一度にwriteされることはない)。安全に書き込みを行うことができる。

pub struct TodoRepositoryForMemory {
    store: Arc<RwLock<TodoDatas>>,
}
enpolioenpolio

ファイル分割

すべてのコードをmain.rsに記述することは現実的ではない。
なのでモジュールを使ってコードを分割する。

同じファイル内でコードを分割する

main.rs
mod module_a {
    pub fn hello_world {
        println!("Hello world!");
    }
}

fn main() {
    module_a::hello_world();
}

modキーワードを使ってモジュールを宣言する。
module_a内の関数hello_worldpubキーワードをつけないとアクセスできない。
Rustでは「あらゆる要素は標準で非公開」というルールになっている。

別ファイルにコードを分割する

こんな感じでファイルを分割する。

root
 ├── main.rs
 └── module_a.rs

先ほどのコードを2つのファイルに分ける。

main.rs
mod module_a; // ここがポイント!

fn main() {
    module_a::hello_world();
}
module_a.rs
mod module_a {
    pub fn hello_world {
        println!("Hello world!");
    }
}

ポイントはmain.rsmod module_a;と宣言すること。これによりmodule_amain.rs内で有効になる。
Rustでは、自分で定義したモジュールはルートファイル(main.rsとか)で宣言してあげないと使うことができない。

useキーワード

毎回module_a::hello_world()ってやるのしんどい。
useキーワードを使うことで、宣言済みのモジュールをスコープに持ち込むことができる。

main.rs
mod module_a;
use module_a::hello_world;

fn main() {
    hello_world();
}

useキーワードを使っても、自作モジュールをルートファイルで宣言する必要はある。

また、パスの指定もできる。

// 絶対パスでの指定
// crateがルートを表す
use crate::front_of_house::hosting::add_to_waitlist;

// 相対パスでの指定
use front_of_house::hosting::add_to_waitlist;

https://doc.rust-jp.rs/book-ja/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html

参照

https://keens.github.io/blog/2018/12/08/rustnomoju_runotsukaikata_2018_editionhan/

enpolioenpolio

Desctructuring

JSやTSで言うところの分割(Destructuring)代入みたいなこと
JSやTSと同様に、タプルやスライス、構造体などで分割代入できる

https://rust-lang.github.io/rfcs/2909-destructuring-assignment.html

(a, b) = (3, 4);
[a, b] = [3, 4];
Struct { x: a, y: b } = Struct { x: 3, y: 4};

// これらは以下の糖衣構文
{
    let (_a, _b) = (3, 4);
    a = _a;
    b = _b;
}
{
    let [_a, _b] = [3, 4];
    a = _a;
    b = _b;
}
{
    let Struct { x: _a, y: _b } = Struct { x: 3, y: 4};
    a = _a;
    b = _b;
}

ネストされててもデストラクチャできる。

let (a, b, c);
((a, b), c) = ((1, 2), 3);

// これは以下の糖衣構文
let (a, b, c);
{
    let ((_a, _b), _c) = ((1, 2), 3);
    a = _a;
    b = _b;
    c = _c;
};

関数の引数でデストラクチャを行うことができる。

// タプル構造体のパターン
struct IpV4Address(u8, u8, u8, u8);

// タプル構造体を引数でデストラクチャ
fn print_ipv4addr(IpV4Address(o1, o2, o3, o4): &IpV4Address) {
    println!("{}.{}.{}.{}", o1, o2, o3, o4);
}

// これと同じ
fn print_ipv4addr(address: &IpV4Address) {
    let o1 = address.0;
    let o2 = address.1;
    let o3 = address.2;
    let o4 = address.3;
    println!("{}.{}.{}.{}", o1, o2, o3, o4);
}

fn main() {
    let addr = IpV4Address(127, 0, 0, 1);
    print_ipv4addr(&addr);
}

https://www.possiblerust.com/guide/how-to-read-rust-functions-part-1#destructuring

https://qiita.com/kerupani129/items/f30596eed4e5b2ca7cd1

enpolioenpolio

Serdeについて

SerdeはRustのデータ構造をSerialize/Deserializeする。

  • Serialize : プログラムのデータ構造 → 文字列・バイト列
  • Deserialize : 文字列・バイト列 → プログラムのデータ構造

Serdeを使うことによってStruct, HashMap, etc⇔JSON, YML, etcのデータ変換を行うことができる。

httpリクエストでJSONをreq/resしたりするときに便利。

use serde::{Serialize, Deserialize};

// deriveを使うことで簡単にSeriarive/Deserializeできる
#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let point = Point { x: 1, y: 2 };

    // Point構造体をJSONに変換する
    let serialized = serde_json::to_string(&point).unwrap();
    println!("serialized = {}", serialized);

    // JSONをPoint構造体に変換する
    let deserialized: Point = serde_json::from_str(&serialized).unwrap();
    println!("deserialized = {:?}", deserialized);
}

実行結果

serialized = {"x":1,"y":2}
deserialized = Point { x: 1, y: 2 }

serde_json::to_string()Seriarizeトレイトを実装した型を受け取り、StringResultを返す。
serde_json::from_str()は文字列を受け取り、Seriarizeトレイトを実装した型のResultを返す。

https://serde.rs/

https://qiita.com/garkimasera/items/0442ee896403c6b78fb2

enpolioenpolio

if let と while let

matchを使う際、すべてのパターンを列挙する必要がある。
if let記法を使うと1つのパターンにマッチする場合の処理のみを記載できるので、冗長性を排除できる。

if let Some(3) = some_u8_value {
    println!("three");
}

// これと同じ
let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

同じようなことがwhile式でもできる。
あるパターンにマッチするときループに入る、みたいなことを書ける。

let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

// stack.pop()がNoneじゃない限りループに入れる
while let Some(top) = stack.pop() {
    // pop変数を使える。分割代入的なイメージ。
    println!("{}", top);
}

https://doc.rust-jp.rs/book-ja/ch06-03-if-let.html

https://doc.rust-jp.rs/book-ja/ch18-01-all-the-places-for-patterns.html

enpolioenpolio

axumに関して

axumはRustのweb applicationフレームワーク、tokioやhyperを裏で使っている

routingモジュール

ルーティングの設定を行う。

use axum::{Router, routing::get};

let app = Router::new()
    .route("/", get(root))
    .route("/foo", get(get_foo).post(post_foo))
    .route("/foo/bar", get(foo_bar));

handlerモジュール

ハンドラの設定を行う。
ハンドラはリクエストを処理するための非同期関数(async function)。
0個以上のextractorを引数に取り、レスポンス(に変換できるもの)を返り値にする。

extractorsモジュール

受け取ったリクエストを処理してハンドラに渡す役割を果たす。
FromRequestもしくはFromRequestPartを実装した型・トレイト。

例として、Jsonはextractorである。
(リクエストボディを受け取ってJSONとしてdeserializeしハンドラに渡すので)

use axum::extract::Json;
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    email: String,
    password: String,
}

async fn create_user(Json(payload): Json<CreateUser>) {
    // ...
}

responsesモジュール

レスポンスを扱う。
axumにおいてレスポンスはIntoResponseが実装されハンドラから返されるものと言える。
IntoResponseは大体の型に対し実装されているのでほとんどレスポンスとして扱える。

use axum::{
    body::Body,
    routing::get,
    response::Json,
    Router,
};
use serde_json::{Value, json};

async fn json() -> Json<Value> {
    Json(json!({ "data": 42 }))
}

let app = Router::new()
    .route("/plain_text", get(plain_text))
    .route("/json", get(json));