Javaによる関数型プログラミング、その2

二つのやり方

これまでの問題点はこうだ。これ以上CPUは速くならないから、一つのCPUパッケージに複数のCPUを入れることにした。それらはそれぞれ独立して動作する。

しかし、旧態依然としたプログラムの作り方では、それを活かすことはできない。順番に逐次行うという処理になるからだ。これを活かすには、次のうちの一つしかない。

  • 複数のスレッドを用いて、それらが共通にアクセスする資源(状態)について競合制御を行う。
  • そもそも共通アクセス資源(状態)を排除する。

プログラムが複雑な場合、前者が非常に難しいことは良く知られている。制御しすぎると、かえって遅くなってしまうこともあるかもしれない。

例えば、Aという状態があるとして、XとYとZというスレッドがそれにアクセスする場合、Xが排他的に利用中はYとZは全く利用できない。その間待つしかないのだ。しかも、このようなプログラムをデバッグするのは非常に難しい。

状態を排除するとは?

だから、「そもそも状態なんか排除すればいいんじゃね?」という発想になるわけだ。状態が無いので、他のスレッドが何をやっているのか気にする必要はない。では、具体的に状態を排除するとはどういうことかと言えば、単純なことだ。

  • オブジェクトをImmutableにする。
  • 関数は入力に対して出力を返す。それ以外のことは一切やらない。

Immutableオブジェクト

おそらく関数型言語は、はなからオブジェクトがImmutableであると思うが、Javaではそうはいかない。どんなものかと言えば、作成された後は中身が変更されることは無いオブジェクトだ。例えば、以下になる。

class NameSlot {
  final String name;
  final String address;
  NameSlot(String name, String address) {
    this.name = name;
    this.address = address;
  }
}

このオブジェクトは作成された後は、一切変更されることは無い。したがって複数のスレッドから同時にアクセスされても安全だ。

入力に対して出力を返す以外はやらない

処理の方は処理の方で、入力されたものに応答して出力を返すだけだ。先の例のaddressを変更したいとすると、こうなるだろう。

NameSlot changeAddress(NameSlot slot, String newAddress) {
  return new NameSlot(slot.name, newAddress);
}

対象となったオブジェクトのaddressを変更するのは不可なので、新たなNameSlotを作成することになる。

もちろん、この過程において、どこぞの他の変数に保持されているオブジェクトの中身を変更することもしない。こういった副作用が一切無いのだ。

純粋関数

さらに「純粋関数」と言われるものになるためには、「同じ値が入力されたら、常に同じ値を返す」ことが必要なのだそうだ。だから、例えば以下のようなものはダメだ。addressにランダムな番号をつけ加えている。

NameSlot changeAddress(NameSlot slot, String newAddress) {
  return new NameSlot(slot.name, newAddress + new Random().nextInt());
}

どう切り分けるのか?

もちろん、上記の過程はかなり非現実的と言える。少なくともJavaにさんざん付き合ってきた身とすれば、そして、以前にも書いたことだが、実際に状態無しでは一切のプログラミングはできない。画面に表示されるものもある種の状態を持ち、データベース内のデータも状態だ。

では、マルチコアの恩恵を受けつつ所望の目的を達するようなプログラムを作るには、どうすれば良いかといえば、結局のところ「ハイブリッド」の考え方をするしかない。つまり、

  • スピードの求められるところでは関数型
  • 状態操作が求められるところではオブジェクト指向

ということになるのだろう。

実際のところ関数型言語でもすべてを状態無しで行うのは不可能だという。何かしらの抜け道が用意されているようだ(詳細については私は知らない)。

(続く)