多忙な Java 開発者のための Scala ガイド: クラスの動作

Scala のクラスの構文とセマンティクスを理解する

Java™ 開発者が Scala を理解するための最初のポイントとしてオブジェクトに言及することは妥当なことです。この、「多忙な Java 開発者のための Scala ガイド」シリーズの第 2 回である今回は、Ted Neward が言語を評価するための基本前提に従って話を進めます。その前提とは、言語の能力は、新しい機能 (この場合は複素数のサポート) を統合する能力と直接関連させることで評価できる、というものです。この記事では、そうした言語の能力を説明する中で、Scala でのクラスの定義と使い方に関連する興味深い内容についてもいくつか学びます。

Ted Neward, Principal, Neward & Associates

Ted Neward photoTed Neward は、Neward & Associates の代表として、Java や .NET、XML サービスなどのプラットフォームに関するコンサルティング、助言、指導、講演を行っています。彼はワシントン州シアトルの近郊に在住です。



2008年 2月 19日

先月の記事では、Scala の構文のごく一部にのみ触れましたが、それは Scala プログラムを実行して、その簡単な機能を知るための最低限にすぎません。前回の記事の Hello World の例とTimer の例で紹介することができたのは、Scala の Application クラスと、メソッド定義および匿名関数の構文、Array[] のほんの一端、そして型推論の一部です。Scala には、まだ他にも非常に多くの機能があります。そこでこの記事では、Scala でコーディングをする際の難解な部分について詳しく調べることにします。

このシリーズについて

このシリーズでは、Ted Neward が皆さんと共に Scala プログラミング言語を深く掘り下げます。developerWorks の、この新しいシリーズでは、Scala が最近もてはやされている理由を調べ、Scala の言語機能の実際の動作を調べます。Scala のコードと Java のコードの比較が重要な場合には両者のコードを並べて示しますが、(これから学ぶように) Scala の機能のうちの多くは、Java には直接対応するものがありません。そして Scala の魅力の多くがあるのはそこなのです。結局のところ、Java コードで可能ならば、手間をかけて Scala を学ぶ必要はないのです。

Scala の関数型プログラミング機能は非常に強力ですが、Java 開発者が Scala に興味を持つのは関数型プログラミングの機能があるという理由からだけではありません。実際、Scala は関数型の概念とオブジェクト指向とを融合しており、Java プログラマー兼 Scala プログラマーがもっと Scala に親しみを持ってもらうためには、Scala のオブジェクトの特徴に注目し、その特徴が言語面で Java とどのように対応するのかを調べた方が賢明です。注意して欲しいのですが、このオブジェクトの特徴の中には直接 Java に対応するものがない場合や、直接対応しているというよりは類似していると言った方がよい場合があります。そこで、その違いが重要な場合には、それを指摘することにします。

Scala のクラス

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 キーワードを使うことができます。)


コアとなるいくつかの値

次は、numerdenom の定義です。これらに関係する構文を見ると、Java プログラマーはすぐに、numerdenom がそれぞれ 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] です。これはまさに期待通りの値です。


Java に隠れた Scala

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
}

SUnit

Scala ベースの単体テスト・スイートが、SUnit という名前で既に存在しています。リスト 10 に示すテストに SUnit を使用したとすると、リフレクションを使った回避策は必要なくなります。Scala ベースの単体テストのコードは Scala クラスに対してコンパイルされるため、コンパイラーはシンボルを整列させることができます。実際、一部の開発者は、POJO を作成する一方で Scala で単体テストを作成した方が、Scala を使わずにテストをするよりもメリットがあると考えています。

SUnit は標準の Scala ディストリビューションの一部であり、scala.testing パッケージの中にあります (SUnit の詳細は「参考文献」を参照してください)。

上記のテスト・スイートは合理的なことに、Rational クラスの動作を確認することの他に、(演算子に関しては少しばかりインピーダンス・ミスマッチがあるものの) Java コードから Scala コードを呼び出せることも確認しています。もちろん、この素晴らしい点は、Java クラスを Scala クラスにマイグレートしながら、(Java クラスを検証するテストをまったく変更する必要なく) 少しずつ Scala をテストできることです。

テスト・コードの中で唯一、皆さんが気付きそうな風変わりな点は、演算子の呼び出し (この場合は Rational クラスに対する + メソッド) に関係する部分です。javap の出力を見直してみると、Scala は明らかに + 関数を JVM の $plus メソッドに変換しています。しかし Java 言語の仕様では識別子に $ という文字を許可していません ($ という文字がネスト・クラスの名前や匿名ネスト・クラスの名前に使われているのはこのためです)。

これらのメソッドを呼び出すためには、Groovy あるいは JRuby (あるいは $ という文字に制限を課さない何らかの他の言語) でテストを作成するか、あるいは、そのメソッドを呼び出すためのリフレクション・コードを少しばかり作成する必要があります。私は後者の方法を採用することにします。この方法は Scala の観点からはあまり面白くありませんが、興味のある方のために、その結果をこの記事のコード・バンドルの中に含めてあります。(「ダウンロード」を参照してください。)

こうした回避策は Java の識別子として許可されていない関数名の場合にしか必要ないことに注意してください。


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 articlej-scala02198-code.zip2.6MB

参考文献

学ぶために

製品や技術を入手するために

  • Scala をダウンロードし、このシリーズと共に Scala の学習を始めてください。
  • SUnit は標準の Scala ディストリビューションの一部として scala.testing に含まれています。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=295549
ArticleTitle=多忙な Java 開発者のための Scala ガイド: クラスの動作
publish-date=02192008