前回の(1)はこちら から。
JSONエンコードをわかりやすく変更しやすくする
ISUCON11のJSONエンコードでは、Cpanel::JSON::XS
のバージョン4.26を利用しました。次のコードは、文字列としてJSONエンコードしたいならばJSON_TYPE_STRING
を、整数値ならばJSON_TYPE_INT
を、真偽値ならばJSON_TYPE_BOOL
を指定し、Cpanel::JSON::XS::Type
で型を明示している例です。
use Cpanel::JSON::XS;
use Cpanel::JSON::XS::Type;
# どうエンコードしたいか第二引数に指定できる
encode_json({
name => "foo",
age => 10,
flag => 1
}, {
name => JSON_TYPE_STRING,
age => JSON_TYPE_INT,
flag => JSON_TYPE_BOOL,
});
# => {"name":"foo","age":10,"flag":true}
以降では、JSONエンコードを巡る罠や、これまでISUCONで利用されてきたJSON::Types
との違いに触れ、Cpanel::JSON::XS::Type
の利点を説明します。
JSONエンコードの罠たち
PerlのJSONエンコードでは、Perlの柔軟さゆえ罠がいくつかあり、これらの罠はCpanel::JSON::XS::Type
やJSON::Types
といったモジュールの開発動機になります。
数値が、意図せず文字列になる罠
複数のプログラミング言語を使うとき、JSONの解釈の違いが問題になることがあります。たとえば、ISUCONでWebサービスのチューニング後も仕様を満たしているかを確認する際、JSONの型が異なれば不正となります。
具体的な例で説明すると、数値の12
を1つ持つ配列をJSONエンコードすれば、通常は期待どおり[12]
となりますが、次のコードのように(2) で出力をすると、JSON::XS
のバージョン4.03のエンコードの結果は、意図せず["12"]
となり12
は文字列扱いされます。
use JSON::XS;
my $num = 12;
warn "[DEBUG] num: $num \ n";
encode_json([$num]); # => ["12"]
この原因は、本連載第16回「Perl内部構造の深遠に迫る 」で丁寧に解説されています。簡単にまとめると、(1) ではPerlの内部的には整数値(IV
、Integer Value )だった$num
が(2) で文字列コンテキストとして評価され、$num
は文字列(PV
、Pointer Value )と整数値(IV
)の両方の性質を持つPVIV
となります。ここで問題なのが、もともとが数値なのか文字列なのかを判断できなくなることです。どちらの型でエンコードすべきかは、開発者の判断になります。この事情からJSON::XS
では、整数値のフラグが立っていても文字列のフラグが立っていれば文字列として扱うため、["12"]
となります。一方、Cpanel::JSON::XS
やJSON::PP
のバージョン2.91_01以降では数値として扱い、[12]
となります。いずれにしろ、期待どおりにJSONエンコードされない不安を抱えます。
Perlで偽となる値が、JSONで偽とならない罠
1==0
や!!0
をJSONエンコードした場合、false
となることを期待しますが、次のコードが示すとおり現状そうはなりません。false
とエンコードしたい場合、\ 0
(0のリファレンス)やJSON::XS::false
などを利用する必要があります。
encode_json([1 == 0]); # [""]
encode_json([!!0]); # [""]
encode_json([\ 0]); # [false]
encode_json([JSON::XS::false]); # [false]
現在開発版であるPerl 5.35.4 からは、Perlの判定式の結果を変数に代入しても保持するようになるため、JSONのシリアライズがより直感的になりそうです。
罠をシンプルに解決するJSON::Types
過去のISUCONで利用されていたJSON::Types
は、次のコードのとおり、数値が欲しければnumber
関数を、真偽値が欲しければbool
関数を挟み、JSONエンコードの型を指定し、正しくエンコードする手段を簡潔に提供します。
use JSON::Types;
my $num = 12;
warn "[DEBUG] num: $num \ n";
encode_json([number $num]); # => [12]
encode_json([bool !!0]); # => [false]
中身はシンプルで、number $num
であれば$num + 0
と等価で、bool !!1
であれば!!1 ? \ 1 : \ 0
と等価で、期待の型でエンコードされる変換を行っています。
JSON::Typesの弱点
JSON::Types
は簡潔で使いやすいユーティリティですが、いくつか弱点があると感じます。
1つ目は、JSON::Types
のnumber
関数で数値を期待する処理をしたあとに、もし文字列評価すれば、期待と異なる文字列になることです。
2つ目は、意図しないJSONエンコードが発生したとき、原因の追跡は骨が折れることです。次のコードは、この2つの問題が複合した例です。number $row->{id}
したあとに、速度改善目的で$cache->{$row->{id}}
とキャッシュを取り出すコードです。
my $row = { id => 123 };
my $cache = { 123 => "hello" };
$row->{id} = number $row->{id};
$row->{value} = $cache->{$row->{id}};
encode_json($row);
# {"id":"123","value":"hello"}
これのJSONエンコードの結果は、id
が意図せず"123"
と文字列になります。原因は、ハッシュリファレンスのキーが文字列コンテキストで評価され、$row->{id}
がIV
からPVIV
になったためです。このコードであれば、$cache->{$row->{id}}
を疑えるかもしれませんが、明確に理解するにはコンテキストの知識が必要です。これは気付きにくい不具合です[1] 。
3つ目は、ネストされる複雑な構造をJSONエンコードするとき、コードが複雑になることです。次のコードでは、$detail->{id}
をnumber
で変換するために二重ループしています。また、@json_items
の構造、つまりどんなJSONが得られるのかがわかりにくいです。そのため、正しい変更を行えたか確認しづらいです。このコードはもっと簡潔に書けるかもしれませんが、JSONエンコードを頑張るよりほかを頑張りたいところです。
use JSON::XS;
use JSON::Types;
my @json_items;
for my $item ($items->@*) {
my @json_details;
for my $detail ($item->{details}->@*) {
push @json_details => {
id => number $detail->{id},
timestamp => number $detail->{timestamp},
}
}
push @json_items => {
name => string $item->{name},
details => \ @json_details,
}
}
encode_json(\ @json_items);
Cpanel::JSON::XS::TypeでJSONエンコードの罠とおさらば
ISUCON11のPerl実装では、JSONエンコードを正確にしつつ、複雑な構造でもデバッグしやすくするため、Cpanel::JSON::XS::Type
を利用しました。次のコードは、期待するJSONの型を宣言し、JSONエンコードしています。
# 期待するJSONの構造を宣言
use constant ItemDetail => {
id => JSON_TYPE_INT,
timestamp => JSON_TYPE_INT,
};
# Itemは、ItemDetailのリストをネストしている
use constant Item => {
name => JSON_TYPE_STRING,
details => json_type_arrayof(ItemDetail),
};
# 取得してきたデータ($items)を
# 型と一緒に渡してJSONエンコードする
encode_json(
$items,
json_type_arrayof(Item)
);
このコードは、JSON::Types
の弱点を克服します。
まず、encode_json
する際、第二引数に期待するJSONの構造を渡しています。そのため、意図せぬ型変換が挟まれる余地はありません。また、複雑なネスト構造があっても、上記のコードのItemDetail
やItem
のような部品ごとに構造を書けます。
さらに、期待するJSON構造が明示されることで、正しく動作するか判断しやすくなり、変更しやすくなります。たとえば、次のコードのように、キーの名前を打ち間違えるとエラーが出ます。
encode_json({ boo => 1.0 }, { foo => JSON_TYPE_FLOAT });
# => ERROR: no type was specified for hash key 'boo'
まとめ
本稿では、最初にわかりやすさや変更しやすさの意味を掘り下げました。わかりやすさは意味を理解するまでの認知の円滑さと定義し、効果的に意味を理解するための方法を解説しました。また、わかりやすさだけでは十分な品質ではなく、変更に備えた設計が必要と書きました。
次に、ISUCON11の参考実装で利用した内容を例にして、Perlの4つ新機能が不要な罠を回避し、コードをわかりやすくすると紹介しました。
最後に、PerlでのJSONエンコードでありがちな罠とその回避方法を解説しました。Cpanel::JSON::XS::Type
で期待するJSON構造を明示することで、変更しやすさにもつながります。
開発の事情でPerlのバージョンを変更することが難しかったり、TMTOWTDI(There's More Then One Way To Do It 、やり方は一つじゃない)というPerlのモットーのとおり、ほかにも良い書き方はあると思いますが、今回の記事が、わかりやすさ、変更のしやすさを考えるきっかけになれば幸いです。
特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT