こちらの記事は2021年度の「株式会社カケハシ x TypeScript」アドベントカレンダーの6日目の記事になります。
さてオブジェクトの同一性(identity)とはなんでしょうか?よく混同しがちな等価性(equality)と共に解説します。
オブジェクトの同一性(identity)とは
オブジェクトが時間を経て変化していったとしても(特定の属性が変わっても)、オブジェクトの識別子(identifier)が同一であれば、同一とみなします。(ex. 人が結婚して名字属性が変わっても、その人がその人であることに変わりない)。個々のオブジェクトは識別子を持っている場合があります。このようなオブジェクトをドメイン駆動設計の文脈ではエンティティと呼んでいます。識別子の定義がオブジェクトの同一性を決定しますが、識別子はソフトウェアの問題領域によって異なる場合があります。
オブジェクトの等価性(equality)とは
等価とは、オブジェクトの持つ値が等しいことです。
JavaScript(TypeScript)における同一性/透過性の設計の問題意識
概念レベルでの同一性を設計に落とし込むことが重要ですが、プログラミング言語の仕様とうまく接合できない場合があります。例えば、JavaScriptの===(strict equality, identity)、==(equality)はオブジェクトのインスタンスが異なれば同一でも等価でもない言語仕様になっています。また、同一性(identity) のことを厳格な等価性(strict equality)としています。
SetやMapなどのコレクションでも言語仕様の等価性を元に判断されます。
fp-tsのEqを使った同一性と等価性
では、fp-tsのEqを使って、同一性と等価性をユーザー定義し、利用していきましょう。
上記Eqの定義はシンプルです。equalsは同じ型のxとyが同値であるかを判定します。具体的な使い方はfp-tsの公式ドキュメントで紹介されていますので参考のために載せておきます。
等価性の定義と適用
- S.EqはEq<String>のインスタンスです
- N.EqはEq<number>のインスタンスです
S.EqとN.Eqを利用してEq<Account>インスタンスを生成しています。Accountに含まれる全てのプロパティが同値であれば、Accountオブジェクトは同値と見なされます。
このように、定義した等価性についてオブジェクトの比較だけでなくコレクションを扱う関数において適用することができます。
ここで、fp-ts/Setを利用した関数を利用していますが、扱っているデータ型をJavaScriptのSetです。fp-ts/Setを使うことによって変わるのは、SetについてEqの型クラスインスタンスを利用して関数を呼び出すことが可能になることだけです。Eqの型クラスインスタンスはアドホックに利用できます。
同一性の定義と適用
アカウントIDを識別子とみなした時の同一とする定義です。
定義した同一性の検査も意図通りです。
Eqはアドホックに利用できるので複雑なコレクション処理だけ部分的に導入することも可能です。その反面、利用の矯正力がないのがトレードオフです。
[代替案] equalsとhashCodeを利用したパターンはどうか?
Java的なアプローチですが、Immutable.js#ValueObjectに定義されているequalsとhashCodeを実装することで同一性の表現ができます。ValueObjectを実装したクラス及びオブジェクトを前提とする場合は、Eqパターンよりも矯正力がありそうです。ただし、Immutable.jsのコレクション型でしか利用できないのがトレードオフです。
なお、同一性とequalsについては各所で考察されている内容です。以下に参考記事を載せておきます。
[代替案] 同一性/等価性を評価する独自メソッドはどうか?
オブジェクトに対する明示的な評価がシンプルに表現できますが、SetやMapなどのコレクションの内部のデータ構造の不変条件を満たす上で、自前メソッドは評価されないトレードオフがあります。
まとめ
今回はfp-tsのEqを利用して同一性/等価性を表現してきました。またその代替案を考察しました。それぞれにはトレードオフがあり正解はありませんが、TypeScriptでドメイン駆動設計をする場合や既にfp-tsが導入されている場合は、Eqは有力な選択肢ではないかと私は思います。