株式会社G・B・S株式会社G・B・S技術ブログを開設しました。forever関数でのStackOverflowErrorについて2019/12/19こんにちは。G・B・S第2システム開発部の小路です。書籍Scala関数型デザイン&プログラミング」の第13章「StackOverflowErrorの回避」の冒頭でlazyを利用したforever関数の実装が例として出されているのですが、この箇所(だけとは言わず書籍全体を通して)は頭の中だけで理解しようとすると難解なので、処理イメージをまとめてみました。書籍では次のコードが記載され、以下のように述べられています。IO.forever(PrintLine("Stillgoing..."))p.runを評価すると、数百行の情報が出力された後、プログラムがStackOverflowErrorでクラッシュします。スタックトレースを調べてみると、runが自身を繰り返し呼び出していることがわかります。問題はflatMapの定義にあります。}ここで、上記のforever関数は以下のように定義されています。t}また、PrintLineは以下のように定義されています。}なので、上記のIO.foreverは次のように展開できます。}を便宜上、selfと置きます。t)の”_”は、self.runの結果とみなせるので、”_”をself.runに置き換えてみます。(実際はコンパイルエラーとなりますが、self.runが呼び出されることをイメージするため、このように表記します)そうすると、上記の式は以下のように置き換えられます。}tなので、IO.foreverで取得したIOモナドのrunは以下のように読み替えられます。}.runここで、tはlazyによる宣言がなされています。lazy修飾子がついた変数は初回アクセス時の結果をキャッシュし、以降の参照ではそのキャッシュした値を返します。IO[B]のインスタンス生成は一度しか行われないので、2回目以降のIO[B]は便宜上、cachedIOと置きます。これまた便宜上、呼び出し回数を表すためにcachedIOの後ろに数字を付け足して、cachedIO(N)のように表記します(Nは呼び出し回数)。(cachedIOのインスタンス自体は全て同一インスタンス)そうすると、上記の式は以下のように置き換えられます。IO[B]のrunを実行するには、cachedIO(1)のrunを実行する必要があります。cachedIO(1)のrunを実行するには、cachedIO(2)のrunを実行する必要があります。以下同様に、cachedIO(N)のrunを実行するには、cachedIO(N+1)のrunを実行する必要があります。IO[B]のrunを実行するためには、自身のrunを内部で繰り返し呼ぶ必要があります。その結果、入れ子のrun呼び出しがスタックに積み上げられていくことになります。これがStackOverflowErrorが発生する原因です。これを回避するためにトランポリン化という手法があり、それを使えばStackOverflowErrorが起こらなくなります。これについては、また別の機会で触れたいと思います。受注開発G・B・Sのニュース、採用情報、イベントなどについての記事を書いてます。社内のイベントもSNSをメインに発信して行きます。興味のある方は是非そちらにも目を通して見てください。