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 は、総じては超ステキ言語なのですが、時たま、策士が策に溺れている感があります。そんなところも大好きですが。
こんにちは。
> ブロックを、単なるブレースで括ろうとすると、ここでは部分適用された print からの行継続で、引数と解釈されてしまいますので、do-while-false ループ (C 言語で、複文を、値を返さない式の位置に書く際に使うイディオム) にしています。
わざわざdo-while-falseにせずとも、
printの後を空行にするか、
locally {
}
を使うことで同じ事ができます(locallyはインライン展開されるので実行時のコストは0)。
To: kmizushima さん。ご指摘ありがとうございます、locally ブロックで、できました!
できたのですが、これって 2.8.0 からの feature のようですね。うちでは 2.7.0 と 2.8.0 が混在しているので使いづらいかも…。非インラインの “identity {}” ならばありそうですね、インライン展開されませんけど。いや、自分で定義すればいいのか。
> printの後を空行にするか、
これも若干カッコ悪いかもです…。
本論ではないのですが…
> 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
まだ始めたばかりなので、そもそも Scala コンパイラが Scala で書かれていることすら知りませんでした、参考になります。
[...] 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 [...]
[...] 以前、「Scala: コンストラクタ中の一時オブジェクトに消えて欲しい – Ayutaya.com」などと言っていた私ですが、ちょっと考え違いをしていました。なまじ逆コンパイルなどをしてしまったために、C++~Java 的な構造体指向の発想にとらわれていましたが、文法的に見ると、Scala のクラス~コンストラクタは、静的なクロージャセットの構築だと見た方が自然です。 [...]