【Scala】Futureの落とし穴と参照透過性

最近同じチームのメンバーからScalaを教えてもらっている。この前は並列処理について教えてもらう機会があったが、彼はScalaの教本で最初に出てくるFutureはなるべく使いたくないという。なぜだろうと思っていたが、実際に小さなサーバをチームで実装してみるとその理由がわかった。

例えば以下のようなサンプルコードを考えてみる。

val future = Future {
    println("aaa")
    "bbb"
}

future.map(println)
Thread.sleep(1000)

// aaa
// bbb

これは当然だろう。では future.map(println) を2回呼び出すとどうだろうか。

val future = Future {
    println("aaa")
    "bbb"
}

future.map(println)
future.map(println)
Thread.sleep(1000)

// aaa
// bbb
// bbb

aaa が一度しか出力されない。Futureは一度値が入って完了状態になると、それ以降に呼ばれた場合中の処理をやり直さず、評価された値を記憶してそのまま使うだけになる。これはFutureが参照透過性を満たしていないということになる。

参照透過性(referential transparency)とは、関数型言語の概念の一つ。ある関数を、その関数の評価結果を格納した変数に置き換えても、コード全体の処理およびその結果が全く変わらない状態を指す。 例えば val add = (x: Int, y: Int) => x + y という簡単な関数があったとして、 add(3, 5) と呼ぶところを単に 8 が入った変数に置き換えても、何の影響もない。
これに対し、関数内で何か副作用のある処理をしている場合は、参照透過性が満たされない。上のFutureの例でいうと、Futureの部分を bbb が入った変数に置き換えると、 println("aaa") の処理がなくなってしまう。
このため、Futureの中に複雑な処理を書いてそれを再利用する場合、参照透過性が満たされないことにより、一部の処理が再実行されない可能性が出てくる。Futureを関数と同じ感覚で使うことはできない。

今回のサーバ実装はFutureを小さく分割することで乗り切ったが、サードパーティの並列処理ライブラリには、Futureに相当する機能を参照透過で使えるものが多いとのこと。Futureを多用すること自体がそもそもアンチパターンなのかもしれない。