Scala: コンストラクタ中の一時オブジェクトに消えて欲しい

Scala については、まだまだ学習中の身ですが、このへんを越えないと安心できない気がするので、書きます。

Scala クラスの基本コンストラクタ (primary constructor) では、クラス定義、コンストラクタ定義、フィールド定義が一緒くたに書けるので、大抵の場合は既存の OOP 言語のそれらと比べて、記述がスッキリして、とても良いです。

class Point(val x: Int, val y: Int)
val p = new Point(3, 4)
println(p.x + ", " + p.y) // 3, 4

ところが、コンストラクタ中で一時変数が必要になると、何やら気持ち悪いことになります。

例としては、「コップ本」こと「Scala スケーラブルプログラミング」から。コップ本の最初の OOP の例で、有理数をとりあげています。最大公約数を求めて分母・分子を約分するのですが、基本コンストラクタ内で求めた最大公約数が、このクラスのインスタンスが存続する限りフィールドとして残ってしまいます、もう不要なのに。

class Rational (
  numArg: Int,
  denArg: Int ) {
  def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
  private val n = gcd(numArg.abs, denArg.abs) // GCD を計算
  val num = numArg / n
  val den = denArg / n
  // private だからアクセスはできないものの、この後、n は永続
}
val r = new Rational(4, 6)
println(r.num + ", " + r.den) // 2, 3

うーん…。

なお、基本コンストラクタの引数は、普通に記述すると、インスタンスの private なフィールドになりますが、val や var で宣言すると、値は隠しフィールドに代入され、そこへのアクセサが自動生成されます (「パラメータフィールド」と呼ぶそうです)。どうやら、Scala のアクセスコントロールが JRE のそれよりも細かいので、アクセサで何とかしているようですよ。多分。下記参照:

間違ってたら直します。

追記 (Wed Oct 27 2010): よく考えたら、overide や lazy にも必要ですね。

それならばと、分子・分母を reader にすれば、n は不要にはならないので、気分的には若干マシになります。けれども、何の解決にもなっていません。

class Rational (
  numArg: Int,
  denArg: Int ) {
  def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
  private val n = gcd(numArg.abs, denArg.abs)
  def num = numArg / n // 呼び出し時の評価…
  def den = denArg / n
}
val r = new Rational(4, 6)
println(r.num + ", " + r.den) // 2, 3

どうしたものかといろいろやっていましたが、当然すでに、いろいろと議論があります。

上記を参考にしつつ、いろいろやってみます。要は、一時変数をスタック上に確保できれば良いのです。

まずは、基本 constructor を private にして、代替 constructor から呼ぶことを考えましたが…、残念、通りません。Scala の代替 constructor では、まず最初に基本 constructor を呼ばなければならないルールになっています。インスタンスの初期化以前にいろいろやるな、というお達しでしょうが、厳しいです。

class Rational private ( // コンパイルが通りません
  val num: Int,
  val den: Int,
  dummy: Unit ) {
  def this(num: Int, den: Int) = {
    def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
    val n = gcd(num.abs, den.abs)
    this(num / n, den / n, ())
  }
}

次に、コンパニオンオブジェクトで、ファクトリメソッドを用意します。apply にしたところで、話は同じ。これは悪くなさそうですが、初期化引数の正規化は、ファクトリではなくコンストラクタの仕事にしたいかなぁ…。インスタンス生成のしかたも変わってしまいますし。

class Rational(val num: Int, val den: Int)
object Rational {
  def create(num: Int, den: Int) = {
    def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
    val n = gcd(num.abs, den.abs)
    new Rational(num / n, den / n)
  }
}
val r = Rational.create(4, 6)
println(r.num + ", " + r.den) // 2, 3

それではと、フィールドの生成に、タプルを使ってみます。一見良いものの、今度は Tuple2 が private フィールドに残ります (自動的に付与される名前の “x$1″ とかで、アクセスできたりする。Scala 的には、メンバとしての位置づけなんだろう)。ですので、かえってデカいんじゃ?

class Rational (
  numArg: Int,
  denArg: Int ) {
  val (num, den) = {
    val n = gcd(numArg.abs, denArg.abs)
    (numArg / n, denArg / n)
  }
  private def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
}
val r = new Rational(4, 6)
println(r.num + ", " + r.den) // 2, 3

逆アセンブルすると、以下のような具合です:

$ javap -c -private Rational
Compiled from "Rational.scala"
public class Rational extends java.lang.Object implements scala.ScalaObject{
private final int den;

private final int num;

private final scala.Tuple2 x$1;

public Rational(int, int);
  Code:
(中略)
$

これで最後。いろいろやってみましたが、一時変数が lexical scope 内に入るので、これが一番綺麗かな? フィールドを、private とはいえ var にせざるを得ないので、アクセスコントロール (リードオンリー) は accessor で行ないます。ブロックを、単なるブレースで括ろうとすると、ここでは print からの戻り値への行継続で、関数の引数と解釈されてコンパイルがコケますので、do-while-false ループ (C 言語で、複文を、値を返さない式の位置に書く際に使うイディオム) にしてみます (素直にセミコロンを置いても良いのですが、カッコ悪いので)。

class Rational (
  private var numField: Int, // 仕方がないので var にします
  private var denField: Int ) {
  print("Initializing ...")
  do { // C でよくやるループ
    val n = gcd(numField, denField) // これはスタック上に確保される
    numField /= n // フィールド変数へ再代入 (実際はアクセサ)
    denField /= n
  } while (false) // 最適化されるので、ループにはならない (多分…)
  println(" Done.")
  def num = numField // public かつ read-only
  def den = denField
  def gcd(l: Int, r: Int): Int = if (r == 0) l else gcd(r, l % r)
}
val r = new Rational(4, 6)
println(r.num + ", " + r.den) // 2, 3

一応、do-while-false がループになっていないことと、num と den がリードオンリーであることと、numField と denField へのアクセスが private のアクセサを経由していることを確認します。

$ javap -c -private Rational
Compiled from "Rational.scala"
public class Rational extends java.lang.Object implements scala.ScalaObject{
private int denField;

private int numField;

public Rational(int, int);
 Code:
  0:  aload_0
  1:  iload_1
  2:  putfield       #13; //Field numField:I
  5:  aload_0
  6:  iload_2
  7:  putfield       #15; //Field denField:I
  10: aload_0
  11: invokespecial  #20; //Method java/lang/Object."<init>":()V
  14: getstatic      #26; //Field scala/Predef$.MODULE$:Lscala/Predef$;
  17: ldc            #28; //String Initializing ...
  19: invokevirtual  #32; //Method scala/Predef$.print:(Ljava/lang/Object;)V
  22: aload_0
  23: iload_1
  24: iload_2
  25: invokevirtual  #36; //Method gcd:(II)I
  28: istore_3
  29: aload_0
  30: iload_1
  31: iload_3
  32: idiv
  33: invokespecial  #40; //Method numField_$eq:(I)V
  36: aload_0
  37: iload_2
  38: iload_3
  39: idiv
  40: invokespecial  #43; //Method denField_$eq:(I)V
  43: getstatic      #26; //Field scala/Predef$.MODULE$:Lscala/Predef$;
  46: ldc            #45; //String  Done.
  48: invokevirtual  #48; //Method scala/Predef$.println:(Ljava/lang/Object;)V
  51: return

(中略)

public int den();
  Code:
   0: aload_0
   1: invokespecial  #58; //Method denField:()I
   4: ireturn

public int num();
  Code:
   0: aload_0
   1: invokespecial  #61; //Method numField:()I
   4: ireturn

private void denField_$eq(int);
  Code:
   0: aload_0
   1: iload_1
   2: putfield       #15; //Field denField:I
   5: return

private int denField();
  Code:
   0: aload_0
   1: getfield       #15; //Field denField:I
   4: ireturn

private void numField_$eq(int);
  Code:
   0: aload_0
   1: iload_1
   2: putfield       #13; //Field numField:I
   5: return

private int numField();
  Code:
   0: aload_0
   1: getfield       #13; //Field numField:I
   4: ireturn

(中略)

}

$

Scala は、総じては超ステキ言語なのですが、時たま、策士が策に溺れている感があります。そんなところも大好きですが。

6 Comments

  1. kmizushima より:

    こんにちは。

    > ブロックを、単なるブレースで括ろうとすると、ここでは部分適用された print からの行継続で、引数と解釈されてしまいますので、do-while-false ループ (C 言語で、複文を、値を返さない式の位置に書く際に使うイディオム) にしています。

    わざわざdo-while-falseにせずとも、

    printの後を空行にするか、

    locally {
    }

    を使うことで同じ事ができます(locallyはインライン展開されるので実行時のコストは0)。

    • knaka より:

      To: kmizushima さん。ご指摘ありがとうございます、locally ブロックで、できました!

      できたのですが、これって 2.8.0 からの feature のようですね。うちでは 2.7.0 と 2.8.0 が混在しているので使いづらいかも…。非インラインの “identity {}” ならばありそうですね、インライン展開されませんけど。いや、自分で定義すればいいのか。

      > printの後を空行にするか、

      これも若干カッコ悪いかもです…。

    • ymnk より:

      本論ではないのですが…
      > locally {
      > }
      > を使うことで同じ事ができます(locallyはインライン展開されるので実行時のコストは0)。

      Predef.locally は確かに @inline つきで定義されていますが、
      コンストラクタ中では展開されない[1]ので、今回の例では少しコストがかかってしまいます。
      なぜ、展開されないような実装になってるのか判りませんが、JVMのJIT(HotSpot)はクラスの
      初期化時には最適化を行わないようなので、それにならっているのかもしせません。

      [1] https://lampsvn.epfl.ch/trac/scala/browser/scala/trunk/src/compiler/scala/tools/nsc/backend/opt/Inliners.scala#L109

      • knaka より:

        まだ始めたばかりなので、そもそも Scala コンパイラが Scala で書かれていることすら知りませんでした、参考になります。

  2. [...] This post was mentioned on Twitter by SEKI Takashi and Shunsuke Sogame, Kiichiro Kyle NAKA. Kiichiro Kyle NAKA said: 備忘録として書きました > Scala: コンストラクタ中の一時オブジェクトに消えて欲しい – Ayutaya.com http://www.ayutaya.com/2010/10/26/scala-constructor-temporary-object/ #scala [...]

  3. [...] 以前、「Scala: コンストラクタ中の一時オブジェクトに消えて欲しい – Ayutaya.com」などと言っていた私ですが、ちょっと考え違いをしていました。なまじ逆コンパイルなどをしてしまったために、C++~Java 的な構造体指向の発想にとらわれていましたが、文法的に見ると、Scala のクラス~コンストラクタは、静的なクロージャセットの構築だと見た方が自然です。 [...]

Leave a Reply