富豪的プログラミングとデータ構造

LLなPythonを使うようになって気づいたのだが、自分が仕事で作成する処理の大部分のデータ構造はユーザ定義のクラスを作成する必要なく、組み込みクラスの組み合わせ済んでしまう。最近仕事でJavaを使うようになって複数のデータをある関数の出力として返したいのでユーザ定義クラスにまとめて返すコードを書いた。すると、処理は(アプリの)汎用的な目的で書いたのに特定のデータ構造に依存してしまうことに気がついた。Javaの関数は複数の出力を書きにくい。なぜなら戻り値が1つしか書けず、Mutableな引数で出力を返すのも分かりずらい。Pythonみたいにタプルで複数返すようにJavaでもリストで返しても良いが、Listとなってしまうのでキャストが必要になりあまりうれしくない気がしてしまう。Javaは複数の出力を返しにくいというのはしょうがない。


言いたかったのはそれではなく、LLだとデータ構造は簡単に変換できるので、関数の界面(インタフェース?)では、以下のように考えたらどうかということだ。例えば、以下のような例を考える。


ここでは、foo1関数とfoo2関数は汎用的な処理を想定している。

class D:
    ...
class E:
    ...

def foo1(a, b, c):
    d = D()
    e = E()
    ...
    return d, e

def foo2(d, e):
    ...

def main():
    d, e = foo1(a, b, c)
    foo2(d, e)

ここで、main関数の処理のfoo1の戻り値に注目したい。foo1の戻り値をfoo2で受け取る引数の型に合わせてしまっている。これだとfoo1とfoo2の関連性が強くなってしまっている。以下のようにクラスを分ければ独立性が高くなり汎用度も高くなる。

class D:
    ...
class E:
    ...
class F:
    ...
class G:
    ...

def foo1(a, b, c):
    d = D()
    e = E()
    ...
    return d, e

def foo2(f, g):
    ...

def main():
    d, e = foo1(a, b, c)
    f = bar(d)
    g = baz(e)
    foo2(f, g)

Javaだと複数の出力が返しにくいということもありユーザ定義クラスを使ってしまう。LLのように組み込み型の組み合わせで出力を返すようにすれば、ユーザ定義クラスを使用する必要なく、データ構造の決定を関数のクライアント側まで遅延できる。しかしJavaでも同様に考えて、foo2の引数で使用するクラス型をfoo1で直接返さずにクライアント側でfoo2に必要な型のオブジェクトに変換すれば、セマンティクスは同じとなる。つまり、foo1で返しているオブジェクトのユーザ定義型のクラスは単なるデータを受け渡しするものとして使用しているということである。


こういう発想というのはJavaだと出にくい気がする。というのも無駄なユーザ定義クラスを局所的に使用するというのは富豪的だからである。しかも狭いスコープにクラスを定義するということもやりずらい。つまり、LLだと富豪的な発想で考えるのにJavaになったとたん富豪的には考えない。そこがJavaで書かれたコードを分かりずらくしている理由の1つだと思う。また、Javaがデータ構造の変換がしにくいということも、そういう発想を出にくくしていると思う。Javaでリストからマップへ変換、マップからリストへの変換、リストのリストからマップのリストへの変換など基本的な変換もしやすいようなやり方がないと思うし、普通はやらない。Pythonならよくやる。


データ構造に関してもう1つ重要だと思うことがある。データ構造とデータは違うということを意識することである。富豪的に考えない慣習のあるJavaでは特にデータ構造の中にデータクラスのオブジェクトを持たせるのではなく、データの中身を展開したものを持たせてしまうということである。Pythonで言うところの__slots__がJavaにはないと思うので、大量のインスタンスを作成するデータクラスのオブジェクトは重たく、Javaではデータクラス専用のメモリコストの安いクラス定義の仕組みがない。私のアイデアは、データ構造の汎用化を考えるとデータを表すクラス(POD; Plain Old Data)をある程度共通に使えるように割と広めスコープで使用して、データ構造を表すクラス(データを保持するクラス)はできるだけ局所的にした方が良いということである。なぜなら、汎用的な処理の界面では、データ構造はいつでも自由に変更できるのでこだわる必要はなく、関数の処理で必要となるデータ構造を引数として渡してもらえば良いからである。汎用的な処理がデータ構造を表すクラス型に足を引っ張られるなどばからしい。


まとめると以下の通り。

  • 汎用的な処理関数の界面は以下のように考える
    • 引数は、処理に必要な素直なデータ構造を受け取る
    • 戻り値は、単に必要なデータをまとめて返せば良い。特定のデータ構造にこだわる必要はない
    • クライアント側(関数の呼び出し側)でデータ構造の変換を行う
  • データ構造型とデータ型の違いとスコープを意識する
    • データ型は汎用度高いので、ある程度広めのスコープで使用して良い
    • データ構造型は汎用度低いケースが多いので、なるべく局所的に使用する