F#にはレコード型がある。レコード型はCの構造体みたいなものだが、 みたいなものという時にはいつも違いが重要になるもので、レコード型もその例に漏れない。 なお、F#に限らずMLにそもそもある概念だった気がするからML派生言語には全部ある気もするが、あんま詳しくないのでF#の話をする。

レコード型は基本はimmutableで変更は出来ない。代わりに部分更新したオブジェクトを新しく作れる。 この辺は最近のJSにも入っているので良くある概念と思う。 さらにハッシュ値とかequalityとか使いそうなものは勝手に生成される。そういうもの。 kotlinのdataクラスとかも似ているが、向こうはvarも指定出来るところはちょっと違う。 ただ基本的な考え方は同じものだ。実際dataクラスも全部valにする事が多くて、実質同じ使い方をする。 という事でここではkotlinのdataクラスも含めてレコード型として話をする。

これらは何かというと、ようするにJavaで言ってた所のValue Objectという奴を作る仕組みだ。 持ってる値だけで同値が定義されて、参照を保持した別の誰かが変更してしまわないという事が保証されているデータ型。 それ自体は自分が知ってる範囲でも90年代のJavaの頃から議論されてた古いアイデアで、実際自分が初めてValue Objectを知ったのは、 Java信者がCOMを批判しているブログを読んでた時だった。ガチで90年代。 当然JavaでもValue Objectは作れる訳だし、Javaコミュニティにも馴染みは深い。 ただ、わざわざValue Objectなんて名前をつけて議論をしていたのは、それが本来もっと作られるべきはずなのに作られていなかったからだ。 というのはJavaでValue Objectを作るのは面倒くさいから。

レコード型はこれを簡単に行う為の言語機能と言える。 言語のコアの所でValue Objectを簡単に作れるようにすると、どうメリットがあるのか、という話をしたい。

レコード型の良い所は、いわゆるPOJOだという所だ。別にPOJOじゃないレコード型も作れるよ、メンバにPOJOじゃないものいれればさ、 というのはまぁそうなんだけど、普通にレコード型作る時はだいたいPOJOだと思う。 レコード型が簡単につくれるので、他のレコード型を含むレコード型が簡単につくれるので、 簡単な構成要素(intとかstringとかのプリミティブなデータ型)を組み合わせた型を、さらに組み合わせていって複雑な構造を表現出来る訳だ。 シンタックス的に簡単につくれるので、たくさん作ってもコードが膨れ上がらない。 だから各問題に合わせて、いわゆる小クラス主義的に直行した小さな型をたくさん作っても面倒が無い。 シンタックスの軽さというのは本質的に重要な所だ。Value Objectを作るのが面倒だったら、延々と小さなValue Objectを実装するなんて非生産的もいいところ。

POJOだけで複雑な構造を表現しやすい、というのは、POJOだけで問題の多くを記述しやすいという事でもある。 プログラムのどこかではPOJOじゃない所がある訳だけど、その部分を小さく出来る。 Javaだって依存は頑張れば切れる訳だし、切りにくい所だってDIにする訳だが、インターフェースはPOJOじゃないしその面倒さはある。 JavaでORMのオブジェクトの集約関係などをlazyさも含めて延々と独自のPOJOなDTOでミラーリングするなんて完全に人生の無駄遣いで、 百害あって一利ない。 でもレコード型の定義が簡単な言語だと、POJOでミラーリングするのが正当化出来るケースがより増える(ただし相変わらず正当化出来ないケースもあるが)。 もちろん本来はミラーリングじゃなくてちゃんとドメインに合わせたモデリングをするんだ、と言うのは簡単だが、本質的に似たような階層が必要になる事はよくあるし、 それを口先だけでごまかしてもミラーリングしている事実は変わらないし面倒な事も消えてはなくならない。

シンタックスが簡単なおかげで小さな型をたくさん作るのが正当化される。そこには振る舞いは無い訳だが、振る舞いが無いおかげで簡単に定義出来るので、 そのおかげでより小さな型をよりたくさん作れるようになる訳だ。 振る舞いを持たせないのは欠点では無くてスタイルの違いと言える。

問題に特化した小さい型は、より多くの制約を表す。それはあらかじめifされているようなものだ。 一番それが顕著に出るのがnull safetyの話なので、nullの話は分かりやすい。nullかどうかをチェックする代わりに、nullかどうかは型で表現し、 nullでないという表明をif文では無く型で行える。 これはnullが一番わかりやすく出て広く知られているというだけで、nullに限らずなんでも言える話だ。個々の型はそれぞれのある種の条件を表している。 あるフィールドが存在するかしないかを判定する代わりに、存在する型と存在しない型を作る訳だ。ifで分けずに型で分ける。

問題に特化した型があるのはJavaだって望ましい事で、特定のフィールドが空だったり値が入っていたりで処理を分けて、それがいろいろな所で散らばるのはgod class的な、良くないデザインだ。 でも一方でJavaで小クラス主義していると、面倒くささもある。 小さなクラスを延々と作っていると、ファイル数も膨れ上がる。 またオブジェクト間のコンバートも楽では無い。 Javaプログラマなら、延々と特定の目的の為に小さなオブジェクトを作らされて、それらを組み合わせてようやく簡単なタスクが実現出来る、 というAPIの面倒くささを皆知っているだろう。 Single Responsibility Principleとかわざわざ言うのは、そう言わないとくっつけたくなるからだ。 そしてさらに厄介な事に、くっつける方が使いやすいAPIデザインな事も多いので、その判断はアーティスティックな物になる。 わざわざ大クラス主義なんて言葉があるのだから言うまでも無いことだが。

レコード型はずっと気軽に作れる。 振る舞いが無いので行数も短いし、振る舞いが無いのでコンストラクタ周辺の自動生成される便利メソッドがより用途決め打ちでいろいろ用意出来る。 使い方もクラスより限定されているのでより多くを自動生成出来る。 パターンマッチやコンストラクタ周辺の自動生成のおかげでレコード間のコンバートも楽だ。 シンタックス的に小さな型がたくさん作れて、それが面倒さをうまない、というのは重要なフィーチャーと言える。 理屈の上ではレコード型だって細かくしすぎて面倒になると思うしきい値はあるはずだが、今の所そう感じた事は無い。 好きなだけ細かくしても面倒さが無い。 これはF#の嬉しい所だ。なんでもかんでもPOJOでモデリングするのが、面倒さが無くむしろ楽。

Hindley milnerの型推論があるのも小さな型をいっぱい作るのを正当化する。 HMのおかげで、ある型から別の型への小さなコンバージョンを行う関数を書く時に、returnの型を指定しなくて良いので簡潔に書ける。 小さな型をたくさん作る時には小さな型を変更する機会が多い事も意味する訳だが、returnの型を指定しなくて良いおかげで型を変更した時の必要な変更量が少ない。でも片付けされてるので直し忘れが無い事は保証される。 変更が楽なだけじゃなくて、型から型への変換だけを行う関数を定義するのも簡潔に書ける。 簡潔に書けるからいっぱいあっても困らない。 細かな型から型への変換が大量にあっても、Javaのようなうんざり感が無い。 小さいfree standing関数を書くのもシンタックス的に軽い、というのも、単純な変換を行うだけの小さな関数を大量に作ってもへっちゃらな事に一役買ってる。 変なclassのstaticメソッドにするとかそんな面倒な事も必要無いからね。

Value ObjectなんてJavaでも作れるじゃん、というのはそうなんだけど、それがシンタックス的に楽な帰結というのは凄く大きい。 シンタックスシュガーは本質的な影響が無いかのように軽視される事もあるけれど、 レコード型があるおかげでプログラムの構造が大きく変わる。 だからそれがより汎用な物で作れるだけじゃ全然十分じゃない。 kotlinのdataクラスはその辺分かってるよなぁ。 わざわざあれを言語機能で入れる大切さってのは、それがプログラムの構造に与える影響を重視している訳だ。

という事で、レコード型とかdataクラスとかって、汎用のclassの機能制限版にちょっとしたシンタックスシュガーに見えるんだが、凄く大切ですよ、って話でした。