Protocol Buffers(proto3)でoptionalをどう扱うか


2021.4.14 追記

proto3で削除されたoptionalですがv3.15(experimentalオプションを利用する場合はv3.12)から正式に実装されたため、それ以降のバージョンを利用する場合は素直にoptionalを利用してもらうのがいいと思います!
https://github.com/protocolbuffers/protobuf/releases/tag/v3.15.0


Protocol Buffersはproto3でrequiredoptionalが削除されました。
そもそも削除された経緯に関しては、@qsonaさんのエントリーにて、分かりやすくまとめて下さっています。

そこで課題になるのが、proto3において各フィールドは全てデフォルト値を持つため、デフォルト値が設定されたフィールドが利用側から
1. 意図的にセットされたデフォルト値と同様の値
2. 存在しないためセットされなかったデフォルト値
のどちらか判断できないということです。

つまりoptionalな値をどのように表現するかということです。

optionalの表現方法

選択肢はいくつかありますが、Stack Overflowの回答が参考になります。

1.別フィールドとして、明示的に有無を表現する

schema.proto
message User {
  int32 id = 1;
  bool has_name = 2; // nameの存在有無を表す
  string name = 3;   // 実際の値
}
Example(Java)
User user1 = User.newBuilder()
        .setHasName(true)
        .setName("Tom")
        .build();

User user2 = User.newBuilder()
        .setHasName(false) // デフォルト値がfalseであるため、実際にはセットも不要
        .build();

user1.getHasName(); // true
user1.getName();    // "Tom"

user2.getHasName(); // false
user2.getName();    // ""

2.oneofを利用する

schema.proto
message User {
  int32 id = 1;
  oneof name_optional {
    string name = 2; // 扱い値をoneofでラップする
  }
}
Example(Java)
User user1 = User.newBuilder()
        .setName("Tom")
        .build();
User user2 = User.newBuilder()
        .build();

user1.getNameOptionalCase(); // User.NameOptionalCase.NAME
user1.getName();             // "Tom"

user2.getNameOptionalCase(); // User.NameOptionalCase.NAMEOPTIONAL_NOT_SET
user2.getName();             // ""

3.google/protobuf/wrappers.protoを利用する

schema.proto
message User {
  int32 id = 1;
  google.protobuf.StringValue name = 2;
}
Example(Java)
User user1 = User.newBuilder()
        .setName(StringValue.newBuilder().setValue("Tom").build())
        .build();
User user2 = User.newBuilder()
        .build();

user1.hasName();            // true
user1.getName().getValue(); // "Tom"

user2.hasName();            // false
user2.getName().getValue(); // ""

考察

まず共通して言えるのは、どの選択肢を選んでも、optionalな値であることを強制できないということです。*1
従ってどの選択肢でも、存在有無のチェック値に対する処理を手続き的に行う必要があります。

1は、記法は非常にシンプルですが、2つのフィールドの関係性をコンパイラでは保証できず、コメント等でサポートする必要があります。

2は、1と比べコンパイラレベルでフィールドの有無を表現できるのがメリットですが、複数フィールドが想定されているoneofからすると少し違和感のある使い方です。

3は、optionalな値を扱う上で直感的で使いやすいですが、いくつかデメリットもあります。

  • wrapper用のBuilderが必要だったり、値を取り出すためにgetValue()を呼び出したりと若干冗長
  • プリミティブでない値をoptionalにしたい場合、1か2と併用する必要があり、optionalの表現方法が2通りになり統一できない

例:

schema.proto
message User {
  int32 id = 1;
  Fullname fullname = 2;
}

message Fullname {
  string first_name = 1;
  string last_name = 2;
}

例えばこのようなケースではwrapperは以下の型しか提供されていないため、fullnameごとoptionalにしたい場合には利用できません。

DoubleValue
FloatValue
Int64Value
UInt64Value
Int32Value
UInt32Value
BoolValue
StringValue
BytesValue

従って、optional表現を統一したい場合には、1か2で統一するのが分かりやすいと思います。

*1: 言語によって実装様々ですが、Maybe、Option、Optional、null安全のような存在しない可能性を強制するもの

まとめ

Protocol Buffersは仕様がコンパクトなスキーマ言語で、学習コストも低く可読性も良いのが特徴です。
しかしそれ故に、他ではよくあるような表現が取り除かれているものもあり、それらを適時カバーしながら使用することでより多くの表現ができると思います。