LoginSignup
10
11

More than 5 years have passed since last update.

トランポリンを試してみよう

Last updated at Posted at 2016-07-14

はじめに

Scala勉強会第176回 in 本郷 : サブテーマ「Scalaで作るウェブサービス」で継続渡しスタイルについて質問して、理解が深まったので今回はトランポリンを紹介したいと思います。

主に、トランポリンを使うとなぜスタックオーバーフローが起こらなくなるのかについて紹介します。

トランポリンとは

要点だけ引用します。

As used in some Lisp implementations, a trampoline is a loop that iteratively invokes thunk-returning functions (continuation-passing style).

引用元: Trampoline (computing) - Wikipedia, the free encyclopedia

日本語に翻訳してみると「いくつかのLispの実装で使用されるように、トランポリンは反復サンクを返す関数(継続渡しスタイル)を呼び出すループです。」となります。

大雑把ですが、サンクを返す関数を呼び出す仕組みくらいで良いと思います。

サンプルコード

継続渡しスタイルでべき乗を計算する関数を考えてみます。

def powCps(x: Int, n: Int, f: Int => Int): Int = 
  if (n == 0) 1 else f(x * powCps(x, n - 1, f))

ひとまず、スタックオーバーフローを確認します。

powCps(1, 10000, identity)
powCps(1, 100000, identity) // java.lang.StackOverflowError

okです :ok_hand:

この関数の定義は末尾再帰ではないのでスタックオーバーフローになります。

末尾再帰を試してみる

末尾再帰を試してみます。

形としては次のようになります:

def powCps(x: Int, n: Int, f: Int => Int): Int =
  if (n == 0) f(1) else powCps(...)

つじつまを合わせると次のようになります:

def powCps(x: Int, n: Int, f: Int => Int): Int =
  if (n == 0) f(1) else powCps(x, n - 1, y => f(x * y))

しかし、これはスタックオーバーフローになってしまいます:

powCps(2, 3, identity) // 8
powCps(5, 2, identity) // 25
powCps(1, 100000, identity) // java.lang.StackOverflowError

特定の場合を机上デバッグしてみる

powCps(2, 3, identity)を机上デバッグしてみます。

以降の机上デバッグですが、見通しをよくするために無名関数の引数の型を省略します。

本来はdef f(x: Int): Int => Int = (y1: Int) => identity(x * y1)のように型を指定する必要があります。

まずn=3のときに、powCpsidentityが渡されます。

呼び出し先で、n=2となり、powCpsy1 => f(2 * y1)が渡されます。

ここでのfidentityなので、渡される関数はy1 => (identity)(2 * y1)となります。

次に、n=1となり、powCpsy2 => f(2 * y2)が渡されます。

ここでのfy1 => (identity)(2 * y1)なので、渡される関数はy2 => (y1 => (identity)(2 * y1))(2 * y2)となります。

次に、n=0となり、powCpsy3 => f(2 * y3)が渡されます。

ここでのfy2 => (y1 => (identity)(2 * y1))(2 * y2)なので、渡される関数はy3 => (y2 => (y1 => (identity)(2 * y1))(2 * y2))(2 * y3)となります。

最後に、f(1)が呼び出されます。これは(y3 => (y2 => (y1 => (identity)(2 * y1))(2 * y2))(2 * y3))(1)と同じことです。

関数が呼び出されると、スタックフレームに戻りアドレスがプッシュされます。

まず、1で関数が呼び出されy3=1となり、(2 * 1)が計算されます。

次に、2で関数が呼び出されy2=2となり、(2 * 2)が計算されます。

そして、4で関数が呼び出されy1=4となり、(2 * 4)が計算されます。

最後に、8で関数が呼び出されy0=8となり、計算結果が8となります。

あとは、この計算結果が呼び出し元に戻っていきます。

一般的な場合を机上デバッグしてみる

一般的な場合は次のようになります:

powCps(x, n - 1, y1 => identity(x * y1))
powCps(x, n - 2, y2 => (y1 => identity(x * y1))(x * y2))
powCps(x, n - 3, y3 => (y2 => (y1 => identity(x * y1))(x * y2))(x * y3))
(以下省略)
(...)(1)

nの値が大きな場合にスタックオーバーフローになることが分かります。

トランポリンを試してみる

まず、トランポリンを用意します。

sealed trait Trampoline[+A] {
  final def runT: A =
    this match {
      case More(k) => k().runT
      case Done(v) => v
    }
}
case class More[+A](k: () => Trampoline[A])
  extends Trampoline[A]
case class Done[+A](result: A)
  extends Trampoline[A]

引用元: 独習 Scalaz — Stackless Scala with Free Monads

次に、トランポリンでべき乗を計算する関数を再実装します。

def powCps(x: Int, n: Int, f: Int => Trampoline[Int]): Trampoline[Int] =
  if (n == 0) f(1) else powCps(x, n - 1, y => More(() => f(x * y)))

呼び出し側は次のようになります:

powCps(2, 3, x => Done(x)).runT // 8
powCps(5, 2, x => Done(x)).runT // 25
powCps(1, 100000, x => Done(x)).runT // 1

一般的な場合を机上デバッグしてみる

ここでも机上デバッグをしてみます。

一般的な場合は次のようになります:

powCps(x, n - 1, y1 => More(() => (y0 => Done(y0))(x * y1)))
powCps(x, n - 2, y2 => More(() => (y1 => More(() => (y0 => Done(y0))(x * y1)))(x * y2)))
powCps(x, n - 3, y3 => More(() => (y2 => More(() => (y1 => More(() => (y0 => Done(y0))(x * y1)))(x * y2)))(x * y3)))
(以下省略)
(...)(1)

特定な場合を机上デバッグしてみる

ここでは一般的な場合の結果を利用して、いろいろなfを確認します。

まずn=0のときにfy0 => Done(y0)である場合、f(1)Done(1)となります。

このDone(1)runTを呼び出すと1が戻ります。

次にn=0のときにfy1 => More(() => (y0 => Done(y0))(x * y1))である場合、f(1)More(() => (y0 => Done(y0))(x * 1))となります。

More(() => (y0 => Done(y0))(x * 1))runTを呼び出すと、((() => (y0 => Done(y0))(x * 1))()).runTとなり、次に、引数なしで呼び出され、そして、((y0 => Done(y0))(x * 1)).runTとなり、最後に、(x * 1)で呼び出され、Done(x * 1).runTとなります。

ここで関数の呼び出しが一旦途切れます。面白くありませんか!?

最後にn=0のときにfy2 => More(() => (y1 => More(() => (y0 => Done(y0))(x * y1)))(x * y2))である場合、f(1)More(() => (y1 => More(() => (y0 => Done(y0))(x * y1)))(x * 1))となります。

More(() => (y1 => More(() => (y0 => Done(y0))(x * y1)))(x * 1))runTを呼び出すと、((() => (y1 => More(() => (y0 => Done(y0))(x * y1)))(x * 1))()).runTとなり、次に、引数なしで呼び出され、そして、((y1 => More(() => (y0 => Done(y0))(x * y1)))(x * 1)).runTとなり、最後に、(x * 1)で呼び出され、More(() => (y0 => Done(y0))(x * x * 1)).runTとなります。

そして、ここで関数の呼び出しが一旦途切れます。

このように関数の呼び出しが一旦途切れることによってスタックオーバーフローにならない仕組みになっています。

参考URL

まとめ

トランポリンを紹介しました。

理解を深めるために机上デバッグをしました。

10
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
11