先月の記事では、Scala の構文のごく一部にのみ触れましたが、それは Scala プログラムを実行して、その簡単な機能を知るための最低限にすぎません。前回の記事の Hello World の例とTimer の例で紹介することができたのは、Scala の Application クラスと、メソッド定義および匿名関数の構文、Array[] のほんの一端、そして型推論の一部です。Scala には、まだ他にも非常に多くの機能があります。そこでこの記事では、Scala でコーディングをする際の難解な部分について詳しく調べることにします。
Scala の関数型プログラミング機能は非常に強力ですが、Java 開発者が Scala に興味を持つのは関数型プログラミングの機能があるという理由からだけではありません。実際、Scala は関数型の概念とオブジェクト指向とを融合しており、Java プログラマー兼 Scala プログラマーがもっと Scala に親しみを持ってもらうためには、Scala のオブジェクトの特徴に注目し、その特徴が言語面で Java とどのように対応するのかを調べた方が賢明です。注意して欲しいのですが、このオブジェクトの特徴の中には直接 Java に対応するものがない場合や、直接対応しているというよりは類似していると言った方がよい場合があります。そこで、その違いが重要な場合には、それを指摘することにします。
Scala がサポートするクラス機能に関して延々と抽象的な議論を始めるよりも、Scala プラットフォームで有理数をサポートできるようにするためのクラスの定義を見てみましょう。(これは主に『Scala By Example』から引用したものです。「参考文献」を参照してください。)
リスト 1. Rational.scala
class Rational(n:Int, d:Int)
{
private def gcd(x:Int, y:Int): Int =
{
if (x==0) y
else if (x<0) gcd(-x, y)
else if (y<0) -gcd(x, -y)
else gcd(y%x, x)
}
private val g = gcd(n,d)
val numer:Int = n/g
val denom:Int = d/g
def +(that:Rational) =
new Rational(numer*that.denom + that.numer*denom, denom * that.denom)
def -(that:Rational) =
new Rational(numer * that.denom - that.numer * denom, denom * that.denom)
def *(that:Rational) =
new Rational(numer * that.numer, denom * that.denom)
def /(that:Rational) =
new Rational(numer * that.denom, denom * that.numer)
override def toString() =
"Rational: [" + numer + " / " + denom + "]"
}
|
リスト 1 の全体的な構造は、語彙の面ではこの 10 年間 Java コードで見慣れてきたものと似ていますが、ここでは明らかにいくつかの新しい要素が登場しています。この定義を分解して調べる前に、新しい Rational クラスを実行するコードを調べてみましょう。
リスト 2. RunRational
class Rational(n:Int, d:Int)
{
// ... as before
}
object RunRational extends Application
{
val r1 = new Rational(1, 3)
val r2 = new Rational(2, 5)
val r3 = r1 - r2
val r4 = r1 + r2
Console.println("r1 = " + r1)
Console.println("r2 = " + r2)
Console.println("r3 = r1 - r2 = " + r3)
Console.println("r4 = r1 + r2 = " + r4)
}
|
リスト 2 は特別興奮するようなものではありません。2 つの有理数を作成し、さらに 2 つの有理数を最初の 2 つの有理数の和と差として作成し、その 4 つの有理数すべてをコンソールに出力しています。(注意: Console.println() は (scala.* の中にある) Scala のコア・ライブラリーの一部であり、(ちょうど Java プログラミングでの java.lang と同じように) すべての Scala プログラムに暗黙的にインポートされます。)
今度は Rational クラスの定義の 1 行目をもう 1 度見てみましょう。
リスト 3. Scala のデフォルトのコンストラクター
class Rational(n:Int, d:Int)
{
// ...
|
リスト 3 を見ると Generics に似た種類の構文を見ているように思う人がいるかもしれませんが、実はこれは Rational クラスにとってデフォルトの、そして推奨のコンストラクターなのです (n と d はこのコンストラクターのパラメーターです)。
Scala ではコンストラクターを 1 つ持つことが推奨されていることは、ある意味で納得できます。というのも、ほとんどのクラスは 1 つのコンストラクターを持つことになるか、あるいは便利なように 1 つのコンストラクターを通じて「チェーン」しているすべてのコンストラクターのコレクションを持つことになるからです。必要な場合には、1 つの Rational クラスに対してさらに多くのコンストラクターを定義することもできます (下記)。
リスト 4. コンストラクターのチェーン
class Rational(n:Int, d:Int)
{
def this(d:Int) = { this(0, d) }
|
推奨のコンストラクター (Int,Int バージョン) を呼び出すことで、Scala のコンストラクターのチェーンは、Java でコンストラクターのチェーンが通常行うのと同じことを行うということに注意してください。
有理数を処理する場合、少しばかり数字のトリックを使うと効果的です。つまり公分母を見つけることで一部の演算を容易にするのです。例えば 1/2 を 2/4 に加算したい場合、Rational クラスは、2/4 が 1/2 と同じであると認識して、2/4 を 1/2 に変換してから加算をするといったような、スマートな処理をする必要があります。
これが、Rational クラスの中にネストされたプライベート関数 gcd() と、プライベートの値 g の目的です。Scala の中でコンストラクターが呼び出されると、クラスの本体全体が評価されます。つまり、g はまず n と d の最大公分母で初期化され、その後で n と d を適切に設定するために使われる、ということです。
前に戻ってリスト 1 を見ると、toString メソッドをオーバーライドして Rational の値を返すように定義していることも容易にわかります。こうしておくと、Rational クラスを RunRational ドライバーのコードから実行する際に非常に便利です。
また、toString の前後の構文にも注目してください。この定義の前にある override キーワードは、対応する定義がベース・クラスの中にあることを Scala が確認できるようにするために必要なものです。こうすることで、タイプミスによって小さなバグが作られてしまうのを防ぐことができます。(これは Java 5 で @Override アノテーションが作られることになったのと動機は同じです。) また、戻り型が指定されていない (メソッド本体の定義から明白です) こと、戻り値が (Java では必要な) return キーワードを使って明示的に表現されているわけではないことにも注目してください。代わりに、この関数の中の最後の値が暗黙的に戻り値と見なされます。(しかし Java の構文を好むのであれば、いつでも return キーワードを使うことができます。)
次は、numer と denom の定義です。これらに関係する構文を見ると、Java プログラマーはすぐに、numer と denom がそれぞれ n/m と d/g の値に初期化されるパブリックの Int フィールドだと思うかもしれませんが、それは正しくありません。
正式には、Scala ではパラメーター無しのメソッドとして numer メソッドと denom メソッドを呼び出します。これらのメソッドは、アクセサー定義用の手軽な構文を作成するために使われます。Rational クラスは 3 つのプライベート・フィールド (n、d、g) を持っていますが、n と d の場合はデフォルトのプライベート・アクセスによって、g の場合は明示的なプライベート・アクセスによって、外部からは隠されています。
皆さんの心の中にいる Java プログラマーはこの時点でおそらく、「n と d のセッターはどこにあるのか」と尋ねているでしょう。しかし、そのようなセッターは存在しません。Scala の強力さの一端は、開発者がデフォルトで不変オブジェクトを作成するように仕向ける点にあります。確かに、Rational クラス内部の値を変更するためのメソッドを作成する構文はありますが、Rational クラスの内部を変更してしまうと、このクラスが持つ暗黙的なスレッド・セーフの性質が台なしになってしまいます。そのため、少なくともこの例では、Rational クラスをそのままにしています。
そうすると当然ながら、Rational クラスを操作するにはどうするのか、という疑問が湧いてきます。java.lang.String と同様で、既存の Rational クラスを取り上げてその内部の値を変更することはできません。そのため、唯一の選択肢は、既存の Rational クラス内部の値を基に新しい Rational クラスを作成するか、あるいはゼロから Rational クラスを作成することです。すると次の一連の 4 つのメソッドに焦点が移ります。これらのメソッドには、+、-、*、/ という珍しい名前が付いています。
そして付け加えて言うと、これらのメソッドは見かけとは異なり、演算子の多重定義ではありません。
Scala ではすべてがオブジェクトであるということを思い出してください。前回の記事では、この原則は関数にも適用され、関数そのものがオブジェクトであるという考えにもなることを説明しました。そのため Scala プログラマーは関数を変数に割り当てたり、関数をオブジェクトのパラメーターとして渡したりといったことができます。それと同じように重要な原則が、すべてが関数であることです。つまりこの特定のケースでは、add という名前の関数と + という名前の関数との間に区別はありません。Scala では、すべての演算子はクラスの中の関数です。たまたま、演算子が変わった名前を持っているにすぎません。
Rational クラスでは、有理数に対して 4 つの演算子が定義されています。この 4 つの演算子とは標準的な数学演算である、加算、減算、乗算、そして除算です。これらはそれぞれ、その演算を表す数学記号 (つまり +、-、*、/) で名前が付けられています。
ただし、これらの演算子は毎回新しい Rational オブジェクトを作成することで動作することに注意してください。これも java.lang.String の動作と非常に似ており、また (これよってスレッド・セーフのコードが生成されるため) これがデフォルトの実装です。(共有状態 (スレッドにまたがって共有されるオブジェクトの内部状態も暗黙的な共有状態です) がスレッドによって変更されることがないならば、その状態に対する同時アクセスの懸念はありません)。
すべてが関数である、というルールによって、次のように強力な効果が 2 つ生まれます。
第 1 の効果として、既に見たように、関数をオブジェクトそのものとして操作し、保存することができます。これによって、このシリーズの第 1 回の記事で説明したような強力な再利用のシナリオが生まれます。
第 2 の効果は、Scala 言語の設計者達が提供しようと考える演算子と、提供されるべきと Scala プログラマーが考える演算子の間に特別な区別がないことです。例えば仮に、分子と分母を逆にして新しい Rational を返す「反転」演算子を提供することが適当だとしましょう (すると Rational (2,5) は Rational (5,2) を返します)。もし皆さんが、この概念を最もよく表すのは ~ という記号だと判断したら、この記号を名前として使用する新しいメソッドを定義することができます。そしてこのメソッドの振る舞いは、Java コードにおける他の任意の演算子の振る舞いとまったく同様です (リスト 5)。
リスト 5. 反転
val r6 = ~r1
Console.println(r6) // should print [3 / 1], since r1 = [1 / 3]
|
Scala でこうした単項「演算子」を定義する場合には少し注意が必要ですが、それは純粋に構文上の問題でしかありません。
リスト 6. 反転の方法
class Rational(n:Int, d:Int)
{
// ... as before ...
def unary_~ : Rational =
new Rational(denom, numer)
}
|
注意が必要な点は、当然ながら、~ という名前の前に「unary_」を付ける必要がある点です。こうすることで Scala コンパイラーに対して、これが単項演算子を意図したものであることを伝えます。従って構文は、大部分のオブジェクト言語で一般的な、従来の「リファレンス、それからメソッド」という構文とは「反転」されます。
これを「すべてのものはオブジェクトである」というルールと組み合わせると、強力な (しかもわかりやすい) コードを作成できることに注目してください。
リスト 7. 加算
1 + 2 + 3 // same as 1.+(2.+(3))
r1 + r2 + r3 // same as r1.+(r2.+(r3))
|
当然ながら、Scala コンパイラーは単純な整数加算の例に対しても「適切な解釈をします」が、構文的にはまったく同じです。これはつまり、Scala 言語の一部として付属している「組み込みの」型と何も変わらない型を作成できるということです。
Scala コンパイラーはさらに、事前定義された何らかの意味を持つ「演算子」(例えば += という演算子など) から何らかの意味を推論しようと試みます。次のコードが (Rational クラスは明示的に += を定義していないにもかかわらず) 実際に行う必要のある処理をしていることに注目してください。
リスト 8. Scala による推論
var r5 = new Rational(3,4)
r5 += r1
Console.println(r5)
|
r5 を出力すると、値は [13 / 12] です。これはまさに期待通りの値です。
Scala が Java のバイトコードにコンパイルできることを思い出してください。これはつまり Scala は JVM 上で実行できるということです。その証拠が欲しいのであれば、コンパイラーが (javac とまったく同じように) 0xCAFEBABE で始まる .class ファイルを生成するという事実を見るだけで十分です。また、JDK に付属している Java バイトコードの逆アセンブラー (javap) を起動し、生成された Rational クラスをその逆アセンブラーにかけたらどうなるかにも注目してください (リスト 9)。
リスト 9. Rational.scala からコンパイルされたクラス
C:\Projects\scala-classes\code>javap -private -classpath classes Rational
Compiled from "rational.scala"
public class Rational extends java.lang.Object implements scala.ScalaObject{
private int denom;
private int numer;
private int g;
public Rational(int, int);
public Rational unary_$tilde();
public java.lang.String toString();
public Rational $div(Rational);
public Rational $times(Rational);
public Rational $minus(Rational);
public Rational $plus(Rational);
public int denom();
public int numer();
private int g();
private int gcd(int, int);
public Rational(int);
public int $tag();
}
C:\Projects\scala-classes\code>
|
Scala クラスの中で定義された「演算子」は、Java プログラミングの最高の伝統に従ってメソッド呼び出しに姿を変えますが、それらの呼び出しは確かにおかしな名前をベースにしているように見えます。このクラスに対して 2 つのコンストラクターが定義されています。1 つのコンストラクターは int 型の引数を 1 個取り、もう 1 つのコンストラクターは int 型の引数を 2 個取ります。また、もし万が一皆さんが、大文字で始まる Int 型を使っていることが java.lang.Integer を使っているかのようで気になるのであれば、Scala のコンパイラーが Int 型をクラス定義の中で通常の Java の基本型 int に変換してくれるということも覚えておいてください。
優れたプログラマーはコードを作成し、偉大なプログラマーはテストを作成する、という話はよく知られています。私はここまで、そのルールを Scala コードに適用せずにいました。そこで、この Rational クラスを従来の JUnit テスト・スイートの中に入れたらどうなるのか見てみましょう (リスト 10)。
リスト 10. RationalTest.java
import org.junit.*;
import static org.junit.Assert.*;
public class RationalTest
{
@Test public void test2ArgRationalConstructor()
{
Rational r = new Rational(2, 5);
assertTrue(r.numer() == 2);
assertTrue(r.denom() == 5);
}
@Test public void test1ArgRationalConstructor()
{
Rational r = new Rational(5);
assertTrue(r.numer() == 0);
assertTrue(r.denom() == 1);
// 1 because of gcd() invocation during construction;
// 0-over-5 is the same as 0-over-1
}
@Test public void testAddRationals()
{
Rational r1 = new Rational(2, 5);
Rational r2 = new Rational(1, 3);
Rational r3 = (Rational) reflectInvoke(r1, "$plus", r2); //r1.$plus(r2);
assertTrue(r3.numer() == 11);
assertTrue(r3.denom() == 15);
}
// ... some details omitted
}
|
上記のテスト・スイートは合理的なことに、Rational クラスの動作を確認することの他に、(演算子に関しては少しばかりインピーダンス・ミスマッチがあるものの) Java コードから Scala コードを呼び出せることも確認しています。もちろん、この素晴らしい点は、Java クラスを Scala クラスにマイグレートしながら、(Java クラスを検証するテストをまったく変更する必要なく) 少しずつ Scala をテストできることです。
テスト・コードの中で唯一、皆さんが気付きそうな風変わりな点は、演算子の呼び出し (この場合は Rational クラスに対する + メソッド) に関係する部分です。javap の出力を見直してみると、Scala は明らかに + 関数を JVM の $plus メソッドに変換しています。しかし Java 言語の仕様では識別子に $ という文字を許可していません ($ という文字がネスト・クラスの名前や匿名ネスト・クラスの名前に使われているのはこのためです)。
これらのメソッドを呼び出すためには、Groovy あるいは JRuby (あるいは $ という文字に制限を課さない何らかの他の言語) でテストを作成するか、あるいは、そのメソッドを呼び出すためのリフレクション・コードを少しばかり作成する必要があります。私は後者の方法を採用することにします。この方法は Scala の観点からはあまり面白くありませんが、興味のある方のために、その結果をこの記事のコード・バンドルの中に含めてあります。(「ダウンロード」を参照してください。)
こうした回避策は Java の識別子として許可されていない関数名の場合にしか必要ないことに注意してください。
かつて私が初めて C++ を学んでいた頃、Bjarne Stroustrup は、C++ を学ぶための 1 つの方法は C++ を「C を改良したもの」として見ることだ、と助言していました (「参考文献」を参照)。今日の Java 開発者はある面で、Scala を「Java を改良したもの」として見るようにするとよいのかもしれません。なぜなら、Scala によって、従来の Java の POJO をより簡潔明瞭に作成できるからです。例えば、Person という従来の POJO を考えてみてください (リスト 11)。
リスト 11. JavaPerson.java (元々の POJO)
public class JavaPerson
{
public JavaPerson(String firstName, String lastName, int age)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName()
{
return this.firstName;
}
public void setFirstName(String value)
{
this.firstName = value;
}
public String getLastName()
{
return this.lastName;
}
public void setLastName(String value)
{
this.lastName = value;
}
public int getAge()
{
return this.age;
}
public void setAge(int value)
{
this.age = value;
}
public String toString()
{
return "[Person: firstName" + firstName + " lastName:" + lastName +
" age:" + age + " ]";
}
private String firstName;
private String lastName;
private int age;
}
|
今度はこれと等価な、Scala で作成した POJO を考えてみてください。
リスト 12. person.scala (スレッド・セーフな POJO)
class Person(firstName:String, lastName:String, age:Int)
{
def getFirstName = firstName
def getLastName = lastName
def getAge = age
override def toString =
"[Person firstName:" + firstName + " lastName:" + lastName +
" age:" + age + " ]"
}
|
元々の Person には可変の (mutable) セッターがいくつかあることを考えると、これは完全な置き換えではありません。しかし、元々の Person にはそうした可変のセッターの前後に同期化コードがないことも考えると、Scala バージョンの方が安全に使用できます。また、もし本当に Person のコード行数を減らすことが目標ならば、getFoo プロパティー・メソッドを完全に削除することもできます。なぜなら Scala は各コンストラクター・パラメーターの前後にアクセサー・メソッドを生成するからです (firstName() は String を返し、lastName() は String を、そして age() は int を返します)。
こうした可変のセッター・メソッドがどうしても必要な場合でも、やはり Scala のバージョンの方が単純です (リスト 13)。
リスト 13. person.scala (完全な POJO)
class Person(var firstName:String, var lastName:String, var age:Int)
{
def getFirstName = firstName
def getLastName = lastName
def getAge = age
def setFirstName(value:String):Unit = firstName = value
def setLastName(value:String) = lastName = value
def setAge(value:Int) = age = value
override def toString =
"[Person firstName:" + firstName + " lastName:" + lastName +
" age:" + age + " ]"
}
|
余談ですが、コンストラクターのパラメーターに var キーワードが導入されていることにも注目してください。詳細は省略しますが、var が指定されていると、コンパイラーはその値が可変であると判断します。その結果、Scala はアクセサー・メソッド (String firstName(void)) とミューテーター・メソッド (void firstName_$eq(String)) の両方を生成します。すると、生成されたミューテーター・メソッドを陰で使用する setFoo プロパティーのミューテーター・メソッドを容易に作成できるようになります。
Scala は、オブジェクト指向の表現力の豊かさを失わずに関数型の概念と簡潔さを融合しようという 1 つの試みです。このシリーズを読んでおそらく気付き始めていると思いますが、Scala はまた、Java 言語に見られる、(後から考えると) 実にひどい構文の問題をいくつか修正します。
この、「多忙な Java 開発者のための Scala ガイド」シリーズの第 2 回である今回は、Scala のオブジェクト機能に焦点を当てました。これによって、関数型というプールに深くまで入らなくても Scala を使い始めることができます。これまでに学んだことをベースにすれば、既に Scala を使い始めることができ、プログラミングの作業負荷を削減できるはずです。何よりも、Scala を使うことによって、Spring や Hibernate など他のプログラミング環境に必要なものとまったく同じ POJO を作成することができます。
しかし潜水服やスキューバ・ダイビングの道具から手を離さないでください。来月は関数型のプールの奥深くへと潜水を開始します。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Sample Scala code for this article | j-scala02198-code.zip | 2.6MB | HTTP |
学ぶために
- 「多忙な Java 開発者のための Scala ガイド: オブジェクト指向のための関数型プログラミング」(Ted Neward 著、developerWorks、2008年1月) はこのシリーズの第 1 回として、何よりも Scala の概要と、並行性に対して Scala が持つ関数型の手法を解説しています。
- 「Functional programming in the Java language」(Abhijit Belapurkar 著、developerWorks、2004年7月) は、Java 開発者の視点から関数型プログラミングの利点と使い方を説明しています。
- 「Scala by Example」(Martin Odersky 著、2007年12月) は簡潔に、コードを中心に Scala を紹介しています (PDF)。
- 『Programming in Scala』(Martin Odersky、Lex Spoon、Bill Venners の共著、2007年12月、Artima 刊) は 1 冊の本の長さで Scala を紹介した最初の資料です。
- C++ を設計し、実装した Bjarne Stroustrup のホーム・ページを見てください。彼は C++ を「より良い C」と表現しています (このコメントは Stroustrup の C++ Programming Language のページで見ることができます)。
- developerWorks の Java technology ゾーンには Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
製品や技術を入手するために
-
Scala をダウンロードし、このシリーズと共に Scala の学習を始めてください。
-
SUnit は標準の Scala ディストリビューションの一部として scala.testing に含まれています。
議論するために
-
developerWorks blogs から developerWorks のコミュニティーに加わってください。
