2020-05-08に投稿

Serde入門 (1) Derive属性の裏側をちょっと覗く

読了目安:17分

Serde入門

趣旨

Bayardという全文検索エンジンがあり保存されているドキュメントはJSON形式で取得できる. serde_jsonを使えば良いのだが, serde_jsonではhierarchical_facetというデータが上手く変換できない.

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
enum Episode {
    Jo,
    Ha,
    Q,
}

#[derive(Debug, Serialize, Deserialize)]
struct Human {
    id: String,
    name: String,
    appears_in: Vec<Episode>,
}

fn main() {
    let ikari_shinji = Human {
        id: "1".to_owned(),
        name: "Ikari Shinji".to_owned(),
        appears_in: vec![Episode::Jo, Episode::Ha, Episode::Q],
    };
    let ikari_shinji_str =
        serde_json::to_string(&ikari_shinji).expect("cant't convert into string");

    println!("{}", ikari_shinji_str); // {"id":"1","name":"Ikari Shinji","appears_in":["Jo","Ha","Q"]}

    let ayanami_rei_str = "
    {
        \"id\": \"2\",
        \"name\": \"Ayanami Rei\",
        \"appears_in\": [\"/episode/Jo\", \"/episode/Ha\", \"/episode/Q\"]
    }";
    let anayami_rei: Human = serde_json::from_str(&ayanami_rei_str).unwrap();
    println!("{:?}", anayami_rei);
}

のayanami_rei_strをdeserializeしたい. 実行するとパニックが起きる.

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error("unknown variant `/episode/Jo`, expected one of `Jo`, `Ha`, `Q`", line: 5, column: 36)', src/main.rs:34:30

実はすごく簡単にこのエラーを消せるのですが, 今回はとりあえずserdeを概観し, Derive属性の裏側を覗き見てみましょう.

Serdeとは?

Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.

Serde - Docs.rs

シリアライズとは?

直列化とか訳されたりします. 一般にデータの直列化とはバイト列や文字列への変換を指します. 通常Rustの構造体などはメモリ上に散らばって存在し, これらを参照することで一つの実体のように見せています. 対してバイト列や文字列はメモリ上に直線的な列を作って保存されています. また文字列にしておけばファイルに保存することができます. デシリアライズはその逆に直列化したデータを復元する作業です. こうすることで特定のデータをRustで読み込んで操作できるようになります.

ここではRustのデータ構造を別のより一般的なデータ形式に変換する操作をシリアライズと定義することにします. デシリアライズはその逆です.

登場人物

  • Rustデータ構造 (以下データ構造)
  • Serdeデータ・モデル (以下データ・モデル)
  • データ形式

データ構造をデータ形式へと変換するのがSerdeの目的でした. 両者を仲介するAPIをデータ・モデルと呼んでいます.

The Serde data model is the API by which data structures and data formats interact.

つまりSerdeは二段階の処理を踏むわけです.

データ構造 ↔️ データ・モデル ↔️ データ形式

こんな感じで説明されるわけですがよく分かりませんよね.

データ形式

現在対応しているデータ形式はData formats -Docs.rsを参照してください.

Serdeの基本

Derive属性

基本的な使い方は簡単です. Derive属性の引数にトレイト指定することでSerialize/Deserializeトレイトをコンパイル時に実装してくれます.

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Point {
    x: i32,
    y: i32,
}

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

    let serialized = serde_json::to_string(&point).unwrap();
    println!("serialized = {}", serialized);

    let deserialized: Point = serde_json::from_str(&serialized).unwrap();
    println!("deserialized = {:?}", deserialized);
}

この際Cargo.tomlでは,

[dependencies]
serde = { version = "1.0", features = ["derive"] }

のようにserdeの追加とfeaturesでderiveを指定しておきます.

Serialize/Deserializeトレイト

Using Derive - Serde

Serialize/Deserializeトレイトで実装すべき振る舞いはserializeとdeserializeだけです.

pub trait Serialize {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer;
}

pub trait Deserialize<'de>: Sized {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>;
}

SerializeとDeserializeでは微妙にシグニチャが違いますが, 引数にSerializer/Deserializerという如何にもな名前の型を取ります.

どうやら具体的な変換方法はこれらに委譲されるようです. また具体的な変換処理はデータ形式で違ってきますので, Serializer/Deserializerトレイトを実装した型が必要になるわけです.

プリミティブ型には全てデフォルトのSerialize/Deserialize実装が存在します.

Serde provides such impls for all of Rust's primitive types so you are not responsible for implementing them yourself, ...

Serializing a primitive - Serde
Implementations on Foreign Types - Serialize
Implementations on Foreign Types - Deserialize

例えばstr型に対しては以下のようなトレイト実装が与えられます.

impl Serialize for str {
    #[inline]
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self)
    }
}

要するに以下のようなコードがあった時に,

#[derive(Serialize)]
struct Human {
    name: String,
    age: u8,
    appears_in: Vec<String>,
}

次のように展開されることになります.

use serde::ser::{Serialize, SerializeStruct, Serializer};

struct Human {
    name: String,
    age: u8,
    appears_in: Vec<String>,
}

// This is what #[derive(Serialize)] would generate.
impl Serialize for Human {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut s = serializer.serialize_struct("Person", 3)?;
        s.serialize_field("name", &self.name)?;
        s.serialize_field("age", &self.age)?;
        s.serialize_field("phones", &self.appears_in)?;
        s.end()
    }
}

これを実際にシリアライズすること考えてみましょう. Human構造体はSerializeトレイトを実装しているのでserializeメソッドが自動導出されています.

fn main() {
    let JO = String::from("Jo");
    let HA = String::from("Ha");
    let Q = String::from("Q");
    let ikari_shinji = Human {
        name: "Ikari Shinji".to_owned(),
        age: 14,
        appears_in: vec![JO, HA, Q],
    };
    // ikari_shinji.serialize(??);
}

これでシリアライズはできるようになりましたが, 具体的な方法はserializerに委ねられます. この時点ではどのようなフォーマットで出力するのか定まっていないので当然と言えば当然ですが, 適切なSerializer型を定義することでikari_shinjiインスタンスをシリアライズできます.

Required methods - serde::ser::Serialize

Serializerトレイト

serializerはシリアライズ用のデータ・モデルでデータのシリアライズ用のAPIを提供します. 型に応じてメソッドが宣言されています. 例えば構造体ならserialize_structというメソッドをシリアライズの時に呼び出しています.

use serde::ser::{Serialize, SerializeStruct, Serializer};

struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

// This is what #[derive(Serialize)] would generate.
impl Serialize for Person {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut s: SerializeStruct = serializer.serialize_struct("Person", 3)?;
        s.serialize_field("name", &self.name)?;
        s.serialize_field("age", &self.age)?;
        s.serialize_field("phones", &self.phones)?;
        s.end()
    }
}

名前はSerde型の前に慣例的にserialize_というプレフィックスがつけられます.

Deserializerトレイト/Visitorトレイト

DeserializeプロセスはSerializeとは少し違います. 単純にいろんな入力が想定される点が違います. そのためDeserializerは更に処理をVisitorに委譲します.

serde::de::Visitor

説明が難しいので具体例としてTomlの場合は見てみましょう. toml::de::from_strには以下のような例が載っています.

use serde_derive::Deserialize;

#[derive(Deserialize)]
struct Config {
    title: String,
    owner: Owner,
}

#[derive(Deserialize)]
struct Owner {
    name: String,
}

fn main() {
    let config: Config = toml::from_str(r#"
        title = 'TOML Example'

        [owner]
        name = 'Lisa'
    "#).unwrap();

    assert_eq!(config.title, "TOML Example");
    assert_eq!(config.owner.name, "Lisa");
}

from_strはこの入力文字列をDeserializerに渡して, deserialize関数内部で処理を実行します.

pub fn from_str<'de, T>(s: &'de str) -> Result<T, Error>
where
    T: de::Deserialize<'de>,
{
    let mut d = Deserializer::new(s);
    let ret = T::deserialize(&mut d)?;
    d.end()?;
    Ok(ret)
}

さて元の例をcargo-expandで展開してみましょう. いろいろコードが増えますが, 実際の呼び出し場所は以下のようになります.

_serde::Deserializer::deserialize_struct(
    __deserializer,
    "Config",
    FIELDS,
    __Visitor {
        marker: _serde::export::PhantomData::<Config>,
        lifetime: _serde::export::PhantomData,
    },
)

Config構造体に対応してdeserialize_structが呼び出されています. __Visitorというマクロ展開によって生成されたインスタンスも渡されています. このインスタンスが実際の処理担うわけです. ここからが実際には大変なところで, 興味がある人は処理を追ってみてください.

まとめ

だいぶ内容がSerializerに偏ってしまいました. Serializeは比較的分かりやすいです. 例えばRustのコードをJSON化する場合どのような対応関係なのかはある程度予想がつきます. 少なくともRust側データ構造は決まっているからです. しかしDeserializeは比較的自由なJSONを型のきっちり決まったRustのデータ構造に仕立て直す必要があり, 自然と複雑化します. 例外はserde_jsonでシリアライズした者を復元する場合だけです.

いずれにしても特殊なデータ形式に対応したいという開発者以外が独自にSerializeとかDeserializeは不要です. 多くの場合は属性を付与することでSerialize/Deserializeの挙動をカスタマイズできます. 次はそれを

課題

BayardのJSONのデータをDeserializeする

[Need help with #serde(deserialize_with)

補足

補足では本論とは関係ないけど気になったことをメモしておきます. 良かったら読んでみてください.

Serdeの設計とSOLID原則

SOLID原則とは,

  • Single Responsibility Principle (SRP)
  • Open Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

の5つの原則の頭文字をとったものです.

LSPにつてはRustには継承を言語レベルで採用していません. ただこの原則が型の互換性(多態性)に関する原則だと見るとトレイト境界で強制できます. serializeメソッドはトレイト境界でSerializerトレイトを実装したSerializer型を引数に取るという条件を課すことでLSPを満たしているとも言えます. そもそも継承だけでインターフェースとかがない頃にメソッドのシグニチャを揃えようというような話だったのかなと想像します. 多態性を実現する手段が継承しかない場合はこのような原則で縛りを設ける必要はありそうです. Rustの場合

(あらゆる型の)スーパークラスであるTと境界を設定したサブクラスは代替可能である

と表現すればそのまま当てはまりそうです.

さてserializeメソッドはシリアライズの実行だけを担い, データの変換はSerializer型が提供するAPIが担いました. 一見すると分けすぎな気もしますが, 様々なデータ形式に対応する時にserialzierを別にしておくことで拡張性が保たれることが分かります. 社会でも指示を出す人と実行する人の役割は違うようにserializeの指示を出すのと実際のデータの変換という役割を適切に分離していると考えることもできます.

ここでSerializerは依存性として外側から引数として渡されるような設計になっています. いわゆる依存性注入(DI)です. すでに述べましたがこの依存性はSerializerトレイトを実装している型であれば互換性があります. そもそもSerialize自体もトレイトであり抽象に依存する良い設計と言えそうです(DIP). Serde可能なデータ形式を追加したい開発者はこの関係に従って(縛られて?)ひたすら実装を行うだけです.

逆にISPは満たしていないように見えます. Implementing a Serializerにも

The Serializer trait has a lot of methods ...

とあります. トレイトの粒度としては荒いと言えます. これは不要なメソッドの実装も取り敢えずしなくてはいけないことになります. ser.rsの実装を見ると萎えます ではカスタマイズが必要な場合独自のSerializer型を実装する必要があるのかというとserde属性を使うことで特定の要素だけをカスタマイズできるようになっています. 例えばデシリアライズでデフォルト値を設定したい場合は

#[serde(default)]

を付与することで可能です.

最後はOCPです. この原則は少し分かりにくいですが以下のように表現されます.

the open/closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"

Open–closed principle - Wikipedia

(機能の追加自体がmodificationじゃないのかという感じはしますが)要するに他のコードへの変更なしに新しい機能を追加できるかということです. Implementing a Serializerのser.rsを見てみましょう. データ構造, Serialize/Serializerトレイトがエンティティで機能の呼び出し側はto_stringという関数です.

pub fn to_string<T>(value: &T) -> Result<String>
where
    T: Serialize,
{
    let mut serializer = Serializer {
        output: String::new(),
    };
    value.serialize(&mut serializer)?;
    Ok(serializer.output)
}

OCPに適うということは, ある機能を拡張した場合に, このロジックに変更がなければ良いわけです. 今オレオレデータ形式がserde可能だとして考えましょう(oreクレートとでも呼びましょうか). ここでserializerに新しい型が対応したとしましょう. 例えばRustがxxx型に対応したとします(例えばf128型とか?あるいはクラスが追加されたとか?). 慣例に倣ってserialize_xxxが必要になりますが, to_stringのロジックに変更は必要ありません. OCPも満たしているようです.

Sedeの面白いところは, Derive属性やserde属性を使うことでエンド・ユーザーはこうした実装を何も考慮しなくてもデータの変換が行えるようにしていることです. 反面黒魔術を多用しているためコードを読むのは難しいですが.

Required methodsとProvided methods

Docs.rsにはこのような分類があるのですがRustでメソッドや関数に実装をオプションにするような構文があるという話は知りません. 私の勉強不足かとも思いましたが, 単純にデフォルトの実装の有無で見分けているようです.

How does Rust know which trait methods are required or provided?

実際にserde::de::Visitorを見てみるとexpectingメソッドはシグニチャの宣言だけです.

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result;

一方visit_boolメソッドはエラーを返すような仕様になっています.

fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
whereE: Error,
    {
        Err(Error::invalid_type(Unexpected::Bool(v), &self))
    }

matchガード

Derive属性で自動導出した場合どのようなSerializeトレイトが実装されるのでしょうか.

use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct Human {
    name: String,
    age: u8,
    appears_in: Vec<String>,
}

fn main() {
    println!("cargo expand")
}

これをcargo-expandで展開すると以下のようなSerializeトレイトが実装されます.

let mut __serde_state = match _serde::Serializer::serialize_struct(
    __serializer,
    "Human",
    false as usize + 1 + 1 + 1,
) {
    _serde::export::Ok(__val) => __val,
    _serde::export::Err(__err) => {
        return _serde::export::Err(__err);
    }
};

適切なSerializerメソッドが呼び出されているのが分かります. Match式は式を後置できるのでmathから波括弧までの間が式で, その戻り値でOkかErrかで判断されます. こういうのはMatch guardsというらしいです. 何らかの形でMatch式が使われていることは予想していたのですが実際のコードを見てみると面白いですね.

triマクロ

例でserde_jsonではなくtomlを使ったのはsede_jsonはserialize/deserializeを呼び出さないことと, triマクロを使っているので説明に適さないと思ったからです.

macro_rules! tri {
    ($e:expr) => {
        match $e {
            crate::lib::Result::Ok(val) => val,
            crate::lib::Result::Err(err) => return crate::lib::Result::Err(err),
        }
    };
}

lib.rs - serde-rs /json

参考

The Serde Book
serde - Docs.rs
toml - Docs.rs

ツイッターでシェア
みんなに共有、忘れないようにメモ

ブレイン

Androidアプリ開発者を目指しています. 興味あることリスト: https://t.co/ew3bb6grdJ Github: https://t.co/9btqysHqWr Qiita: https://t.co/ZVRhjouauX

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!
ボードとは?

関連記事

コメント