タイプセーフな識別子とファントムタイプ

多くの場合、データベースを操作するプログラムは、エンティティの識別子として整数値(たとえば、 long



)を使用します。 しかし、人々は間違いを犯しがちであり、プログラマーはあるタイプのエンティティの識別子を誤って別のタイプのエンティティに使用することができます。 エンティティの識別子が交差する場合、このような問題は長い間気付かれない可能性があり、これは非常に頻繁に発生します。 幸いなことに、型を操作できる言語(C ++)には、この問題に対するかなり簡単な解決策があります。



問題文



私たちのプログラムがいくつかのタイプのエンティティで動作するとします。 たとえば、ウィジェット( Widget



クラス)とガジェット( Gadget



クラス)を使用します。



 class Widget { public: long id() const; // ... }; class Gadget { public: long id() const; // ... };
      
      





エラーの可能性が高いことに加えて、識別子として「生の」タイプを使用すると、コードの可読性が大幅に低下します。 std::vector<long>



std::map<long, long>



ような多くの型を含むコードを理解するのはそれほど簡単ではありません。 型シノニムの使用:



 typedef long WidgetId; typedef long GadgetId;
      
      





プログラマは、 std::map<WidgetId, GadgetId>



などの型を操作することで、より表現力豊かなコードを作成できます。 しかし、このアプローチは読みやすさの問題を解決するだけです。 コンパイラは、 GadgetId



型とGadgetId



型の値に互換性GadgetId



ないと見なされることをまだ認識していません。



コンパイラーに意図を伝えます



これらのすべての番号で混乱しないように、多数の抽象的な識別子を使用して紙を操作する必要がある場合、人はどうしますか? 完全に合理的なアプローチは、 タイプラベルを識別子に追加することだと思います-識別可能なエンティティを意味するプレフィックスまたはサフィックスです。 たとえば、K-12は12ジョブのコンピューターを意味し、P-12は12番目の登録ユーザーを意味します。



さいわい、C ++には、型にラベルを付けることができるメカニズム(テンプレート)があります。 この問題を解決するには、型によってパラメーター化され、識別子を格納するクラスを実装するだけで十分です。



 template <typename ModelType, typename ReprType = long> class IdOf { public: typedef ModelType model_type; typedef ReprType repr_type; IdOf() : value_() {} explicit IdOf(repr_type value) : value_(value) {} repr_type value() const { return value_; } bool operator==(const IdOf &rhs) const { return value() == rhs.value(); } bool operator!=(const IdOf &rhs) const { return value() != rhs.value(); } bool operator<(const IdOf &rhs) const { return value() < rhs.value(); } bool operator>(const IdOf &rhs) const { return value() > rhs.value(); } private: repr_type value_; };
      
      





新しいクラスをガジェットとウィジェットに適用します。



 class Gadget; class Widget; typedef IdOf<Gadget> GadgetId; typedef IdOf<Widget> WidgetId; class Widget { public: WidgetId id() const; // ... }; class Gadget { public: GadgetId id() const; // ... };
      
      





IdOf



クラスの定義方法のおかげで、論理エラーを含む次のコードはコンパイルされません。



 // This won't compile. vector<GadgetId> gadgetIds; gadgetIds.push_back(WidgetId(5)); // This won't compile either. if (someGadget.id() == someWidget.id()) { doSomething(); }
      
      





同じタイプの識別子の操作は正しく機能します。 これでコンパイラーは意図をよりよく理解しました。ウィジェット識別子によってガジェットをロードしたり、間違ったタイプの識別子をベクターに入れたりすることはできなくなります。



異なるタイプの識別子を比較する必要がある場合、または識別子を生の値と比較する必要がある場合は、常に明示的にvalue()



メソッドを呼び出すことができます。



ファントムタイプ



私たちが識別子で思いついたトリックは、かなり前から関数型プログラミングで知られていました。 定義でパラメータータイプを使用しないパラメーター化タイプは、 ファントムタイプと呼ばれます。

たとえば、Haskellでは、同様の手法を次のように実装できます。



 newtype IdOf a = IdOf { idValue :: Int } deriving (Ord, Eq, Show, Read)
      
      





うわー、ほんの数行のコード! 次に、モデルの定義を追加します。



 data Widget = Widget { widgetId :: IdOf Widget } deriving (Show, Eq) data Gadget = Gadget { gadgetId :: IdOf Gadget } deriving (Show, Eq)
      
      





異なるタイプのインスタンスを作成し、それらの識別子を比較して、目的の動作を確認します。



 Prelude> let g = Gadget (IdOf 5) Prelude> let w = Widget (IdOf 5) Prelude> widgetId w == gadgetId g <interactive>:1:15: Couldn't match type `Gadget' with `Widget' Expected type: IdOf Widget Actual type: IdOf Gadget In the return type of a call of `gadgetId' In the second argument of `(==)', namely `gadgetId g' In the expression: widgetId w == gadgetId g
      
      





まあ、コンパイラ(または、私はghciインタープリターを実験に使用しました)は、異なるタイプの識別子の比較を受け入れることを拒否しました。 これはまさにあなたが必要とするものです。



この手法を使用して、通貨ラベルの数値、測定単位、およびプログラムの読者とコンパイラーの両方に役立つその他の情報をバインドできます。



まとめ



1つの小さなクラスを使用するだけで、エラーの検索に費やす時間を大幅に節約できます。 また、このアプローチを使用しても、最適化を有効にしてコンパイルする場合、実行時のプログラムのパフォーマンスとメモリ消費には影響しません。 Haskellバージョンでは、追加のオーバーヘッドも発生しません。



欠点は、もう少し文字を入力(および読み取り)し、同僚にアイデアを説明する必要があることですが、コンパイラによるロジックのより厳密なチェックの利点は欠点を上回ります。



ファントムタイプは、高い信頼性が必要なアプリケーションで人気があり、コンパイラによって自動的に実行される追加チェックごとに会社の損失が削減されます。 特に、Jane StreetでのOCamlプログラミングや、Haskellで書かれたStandard Chartered Bank製品で積極的に使用されています( Don StewartがGoogle Tech Talk 2015で話したように )。



強力なBoost.Unitsライブラリについて言及するしかありません。これにより、自動出力型出力を使用して、異なる型の値に対して型保証された操作を実行できます。



All Articles