関数型とオブジェクト指向は対立するか

よくある関数型とオブジェクト指向は対立するか論についても、自分なりの考えは持つことができた。結論としては対立どころか同じことができる。

この話で"関数型"と言ったときに、昔ながらのC言語の世界をイメージする人と、Haskell等のモダンな純粋関数型言語をイメージする人思い浮かぶものが全く異なるように思う。

自分は昔のガラケー向けの仕事もやっていたことがあり、"昔ながらのC言語の世界"というのが理解できる。すごくおおざっぱに言うと、"昔ながらのC言語の世界"には以下の問題があって、それをオブジェクト指向言語ではそれなりに解決できた。

  1. データに対してそれを操作する関数がどこにあるのかわからない
  2. 関数を実行したときに入力と出力の影響範囲がわからない
  3. 現実世界における"りんごは果物である"的なis-aモデルの抽象化を、プログラム言語に落とし込む方法が統一されてない
  4. a(b(c(d)))) というネストした関数の書き方が、Dに対してA, B, Cを順に行うという処理順と逆になっており理解しにくい

これに対し"昔ながらのC言語の世界"ではどうしたか。3.のモデルの抽象化を構造体のネストやポインタ参照で実現し、関数の第一引数に渡すことをルール化するのが定石だった。

Haskellは構造体的なデータ型に紐づく関数定義とそのインターフェース定義の仕組みを持っている。GolangもRustも純粋関数型ではないがそうなっており、最近の言語のトレンドなのだろう。

インターフェースは継承できるし、データ型もC言語と同じくネストや参照で継承できる。これを使えばオブジェクト指向とほとんど同じ仕組みを実装することができる。

違いはデータ型の値が不変かどうかで、不変の場合はデータ型の値を直接を変更することはできず、返り値として変更されたデータ型のインスタンスを返す必要が出てくる。

逆に言うと、オブジェクト指向言語でもクラス変数を全て不変型にしてしまえば、関数型言語と変わらないともいえる。

基本的にはデータ型はC言語と同じく第一引数に渡す必要がある。このあたりを4.のわかりにくさを解決するためにどうオブジェクト指向らしい文法を許容するかが、最近の言語の特徴だろう。

しかしこのデータ型が巨大になってしまうと、関数型言語のメリットである入力と出力が定義で明確であるというメリットが少なくなってしまう。

やろうと思えば、環境内の全ての変数にアクセスできるデータ型を渡すことも可能だ。ReduxでStoreへのアクセスができることは実質的にこれと同じだ。かつてはBREWがそんな仕組みだった。これを小さく保つことが必要だろう。