Scala チュートリアルの抄訳
自分のお勉強のためにA Scala Tutorial for Java programmers を簡単にまとめてみる.ちゃんとした日本語訳は,こちらにある.
最初の例
最初の例はよくあるHello world.
object HelloWorld { def main(args: Array[String]) { println("Hello, world!") } }
Javaプログラマにはおなじみの構造だろう.mainメソッドの引数はコマンドライン引数を文字列の配列である.メソッドのボディでは,printlnを呼び出している.mainメソッドは,返り値を持たない「procedure method」なので,返り値の型は定義されていない.
Javaプログラマにとって違和感があるのは,「object」という定義だろう.これは,いわゆる「シングルトンsingleton」を定義する宣言で,ひとつしかインスタンスを持たないクラスを定義している.この宣言では,HelloWorldというクラスと,そのクラスのインスタンスで(同様に「HelloWorld」と呼ばれる)を同時に宣言している.このインスタンスは,最初に用いられたときに自動的に生成される.
mainメソッドがstaticで宣言されていないことに気がついただろうか? Scalaにはstaticメソッドもフィールドも存在しない.staticメンバの代わりにシングルトンを用いる.
Javaとの連携
Scalaのメリットの一つは,簡単にJavaのコードと連携することができることである.java.langパッケージのクラスはデフォルトでインポートされるが,他のパッケージは明示的にインポートする必要がある.
例を見てみよう.日付を特定の国,例えばフランスの形式でフォーマットする.
Javaにはこのようなことをするための強力なユーティリティクラスが存在する.Date, DateFormat等である.ScalaはJavaのクラスとシームレスに連携することができるので,同じことをするクラスを独自に実装し直す必要は無い.Javaのクラスをインポートすれば良い.
import java.util.{Date, Locale} import java.text.DateFormat import java.text.DateFormat._ object FrenchDate { def main(args: Array[String]) { val now = new Date val df = getDateInstance(LONG, Locale.FRANCE) println(df format now) } }
Scalaのインポート分は,Javaのそれによく似ているが,より強力である.1行目に示すように,複数のクラスを中括弧でくくって,インポートすることができる.もう一つの相違点は,一つのパッケージ,もしくはクラス内のすべての名前をインポートする際に「*」ではなく「_」を用いることである.これは,Scalaでは「*」が有効な識別子で,例えばメソッド名として利用できるためである.
三行目インポート文は,DateFormat クラスのすべてのメンバをインポートしている.これによって,static メソッドであるgatDateInstanceとstaticフィールドであるLONGが直接利用できるようになる.
mainメソッドでは,まず,JavaのDateクラスを作っている.内容はデフォルトの現在時刻となる.次に,DateFormatをインポートしたgetDateInstanceメソッドを使って,作っている.最後に,このDateFormatを用いて,Dateインスタンスをフォーマットしている.最後の行には,Scalaの面白い文法が現れている.引数を一つだけとるメソッドは,infixシンタックスを用いることができる.つまり,
df format now
は,
df.format(now)
に等しい.
どうでもいいことに思えるかもしれないが,これからいくつかのことが導かれる.そのうちのひとつは次の節で見ることにする.
最後に書いておくが,Javaで定義されたクラスをScalaで継承したり,Javaで定義されたインターフェイスをScalaで直接実装したりするのも用意である.
すべてがオブジェクト
Scalaは,すべてがオブジェクトである,という意味で,ピュアオブジェクト指向言語である.
すべてには,数値や関数がふくまれる.この点はJavaと異なる.Javaでは,基本型(booleanやint)を参照型と区別するし,関数を値として操作することはできない.
数値とオブジェクト
数値はオブジェクトであるので,メソッドを持つ.実際,下の算術式
1 + 2 * 3 / x
は,前節で述べたように,メソッド呼び出しのみでできており,下のメソッド式と等価である.
(1).+(((2).*(3))./(x))
この例から,「+」や「*」がScalaの識別子として有効であることが分かる.
後者の例では数値のまわりにつけられているが,これは必須である.というのはScalaのレキシカルアナライザが,トークンに対して最長マッチでマッチングを行うためである.したがって,下のような式は「1.」「+」「2」の3つのトークンに分割される.
1.+(2)
どうしてこのように分割されるかというと,「1.」が「1」より長いからである.トークン「1.」はリテラル「1.0」として解釈され,整数Intではなく浮動小数点数Doubleのリテラルとなる.
(1).+(2)
のように書くことで1をDoubleでなく解釈させることができる.
関数とオブジェクト
Javaプログラマにとっておそらくはさらに驚きなのは,関数もオブジェクトだということだろう.これによてえ,関数を引数として受け渡したり,変数に格納したり,別の関数の帰り値として返すことが可能になる.この,関数を値として操作する機能は,「関数型言語」と呼ばれる非常に興味深いプログラミングパラダイムの基礎の一つである.
簡単な例で,関数を値として扱うことの有効性を示そう.何かの動作を1秒ごとに行うタイマー関数を考えてみる.どうやって,行う動作を指定したらいいだろう.当然関数だろう.これは非常に単純な形の関数私プログラムで,ユーザインターフェイスで,イベントに対するコールバック関数を登録するために用いられる.
次に示すプログラムでは,oncePerSecond関数はコールバック関数を引数として呼び出される.このコールバック関数の型は,() => Unit と書かれている.これは,引数をとらず,なにも返さない,という意味である(UnitはC/C++のvoidのようなもの).main関数では,oncePerSecondを,timeFliesという端末に文章を書き出すだけのコールバック関数を引数として呼び出している.このプログラムは,"time flies like an arrow'という文を毎秒,永遠に書き出し続ける.
object Timer { def oncePerSecond(callback: () => Unit) { while (true) { callback(); Thread sleep 1000 } } def timeFlies() { println("time flies like an arrow...") } def main(args: Array[String]) { oncePerSecond(timeFlies) } }
端末への書き出しに, System.outを用いないで組み込みメソッドprintlnが用いられていることに注意.
無名関数
さらに改良してみよう.関数timeFliesは一度しか使われていない.このような関数は,oncePerSecondに渡す時に作ることができればその方がいい.Scalaでは無名関数でこれを実現することができる.
object TimerAnonymous { def oncePerSecond(callback: () => Unit) { while (true) { callback(); Thread sleep 1000 } } def main(args: Array[String]) { oncePerSecond(() => println("time flies like an arrow...")) } }
「=>」が無名関数の存在を示している.左辺が引数のリスト,右辺がボディとなる.この場合引数リストは空である.
クラス
すでに見てきたように,Scalaはオブジェクト指向言語であり,クラスという概念を持つ.Scalaのクラスは定義はJavaのクラス定義と似ているが,一つ異なる点がある.Scalaのクラスはパラメータをとることができるのだ.下に,複素数の定義を示す.
class Complex(real: Double, imaginary: Double) { def re() = real def im() = imaginary }
この複素数クラスは実部と虚部を引数としてとる.これらの引数は,Complexのインスタンスを作成する際に,渡さなければならない.new Complex(1.5, 2.3)のように.このクラスには二つのメソッドreとimが定義されており,それぞれ実部と虚部へのアクセスを提供している.
これらのメソッドに返り値の型が定義されていないことに注意してほしい.型はコンパイラが自動的に右辺の型から推論してくれる.
コンパイラは常に型を推論できる訳ではない.面倒なことに,推論できる場合とできない場合を区別する簡単なルールは無い.実際には,推論できない場合にはコンパイラが文句を言うので,問題にはならないが.Scala初心者は,文脈から簡単に型が類推できそうな場合には,型宣言を省略してみて,コンパイラが納得してくれるかを試してみるといいだろう.しばらくすれば,どのような場合に宣言を省略できるのか,直感的にわかるようになるだろう.
引数なしのメソッド
reとimの問題の一つは,うしろに空の括弧をつけなければならないことである.
object ComplexNumbers { def main(args: Array[String]) { val c = new Complex(1.2, 3.4) println("imaginary part: " + c.im()) } }
Scalaではこれを省略することができる.定義の際に括弧を省略すれば良い.
class Complex(real: Double, imaginary: Double) { def re = real def im = imaginary }
インヘリタンスとオーバライド
Scalaのすべてのクラスはなんらかのスーパクラスを継承している.スーパークラスが明示的に指定されていない場合には,scala.AnyRefというクラスがスーパクラスとなる.
Scalaでは,スーパクラスから継承されたメソッドをオーバライドすることができるが,その際にはoverride修飾子で明示的にオーバライドしていることを示す必要がある
class Complex(real: Double, imaginary: Double) { def re = real def im = imaginary override def toString() = "" + re + (if (im < 0) "" else "+") + im + "i" }
Caseクラスとパターンマッチング
プログラムの中にツリー構造が出てくることがよくある.例えばXMLドキュメントのパーズツリーや,red-blackツリーなど.
ここでは,簡単な電卓プログラムを通じて,Scalaでのツリー構造の処理を見ていく.このプログラムは,1+2 とか (x+x) +(7+y)のような式を解釈するものとする.
まず式の表現を決めなければならない.もっとも自然な表現は,ノードをオペレーション,葉を値(変数か定数)とするツリー構造だろう.
Javaでこのような構造を表すには,抽象クラスとしてツリーを定義し,ノード,葉に対してサブクラスを定義することになるだろう.関数型言語であれば代数データ型を同じ目的に使う.Scalaにはこのような目的のために,case classというものがある.これはこの両者のだいたい中間にあたる.下にツリーの型を定義する.
abstract class Tree case class Sum(l: Tree, r: Tree) extends Tree case class Var(n: String) extends Tree case class Const(v: Int) extends Tree
Sum, Var, Constはclassではなくてcase classとして定義されている.case classはclassとは幾つかの点で異なる
- インスタンス生成時に new をつける必要が無い (new Const(5) ではなく Const(5)と書ける).
- コンストラクタパラメータに対して自動的にgetter関数が定義される.
- equals メソッドと hashCode メソッドのデフォルト実装が自動的に定義される.これらのメソッドは,オブジェクトのアイデンティティではなく,内容を利用する
- toStringメソッドのデフォルト実装が与えられる.この実装は, ソース内で利用できる形で出力を行う.例えば x+1の式であれば,Sum(Var(x), Const(1)) が出力される
- 後述のパターンマッチングの対象となる
さて,式が定義できたのでまずは式をなんらかの環境で評価してみよう.例えばx+1を xが5である環境({x->5} と書く)で評価すれば,6になる.
そのためには環境を定義しなければならない.ハッシュテーブルのような構造を使ってもいいのだが,ここでも関数が使える! 環境は,変数名と値を対応づける関数に他ならない.Scalaでは,{x->5} を,
{ case "x" => 5 }
と書くことができる."x"に対して5を返し,それ以外の値に対しては例外を返す.
評価関数を各前に,環境の型に名前を付けておこう.String => Int を使ってもいいのだが,名前を付けた方がプログラムが簡単になるし,変更するのも容易になる.
type Environment = String => Int
さて評価関数を定義しよう.概念的にはとても簡単だ.加算式の値は,それぞれの式の値の加算値で,変数の値は,直接環境から取得すればいい.
def eval(t: Tree, env: Environment): Int = t match { case Sum(l, r) => eval(l, env) + eval(r, env) case Var(n) => env(n) case Const(v) => v }
この評価関数は,tに対するパターンマッチングで機能する.何をしているのか直感的に明らかだろう.
- まず,tree tがSumであるかをチェックする.Sumであるならば左のサブツリーをl に,右のサブツリーをrにバインドして,=> の右辺の評価に進む.r と lをそれぞれeval して加算している.
- Sum でなければ,Varであるかどうかをチェックする.
- つぎに Constであるかどうかをチェックする.
- 最後に,すべてのチェックが失敗すると,パターンマッチの失敗を報告するための例外が発生する.ここではTreeに他のサブクラスが存在しない限り,失敗することはない.
evalをTreeクラスとそのサブクラスのメソッドとして記述してもいいのではないか? もちろんScalaでもそうすることはできる.どちらにするかは,ある意味趣味の問題である.が,拡張性の観点から,いくつか言えることがある.
- メソッドにした場合,新しい種類のnodeを追加するにはTreeのサブクラスを定義するだけでよい.しかし,ツリー構造の操作を記述するには,すべてのクラスをいじらなければならないので面倒.
- パターンマッチングを使うと,これは逆になる.
パターンマッチを詳しく見るために,式の記号微分を考えてみよう.ルールは下記の通り.
これをScalaで実装すると下記のようになる.
def derive(t: Tree, v: String): Tree = t match { case Sum(l, r) => Sum(derive(l, v), derive(r, v)) case Var(n) if (v == n) => Const(1) case _ => Const(0) }
ここでは2つの新しい概念が導入されている.一つは,ガードである.ガードは,case式でifキーワードで書かれる. if部の条件が満たされない場合,パターンマッチは失敗する.この場合,変数nが変数vと同じでない場合にはパターンマッチが失敗する.もう一つの新しい概念は,ワイルドカード「_」である.これは,すべての値に対してマッチする
パターンマッチングに関してはまだまだ書くべきことがあるが,ここでは省略する.プログラムを実際に動かしてみよう.
def main(args: Array[String]) { val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y"))) val env: Environment = { case "x" => 5 case "y" => 7 } println("Expression: " + exp) println("Evaluation with x=5, y=7: " + eval(exp, env)) println("Derivative relative to x:\n " + derive(exp, "x")) println("Derivative relative to y:\n " + derive(exp, "y")) }
結果は下記のようになる.
Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y))) Evaluation with x=5, y=7: 24 Derivative relative to x: Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0))) Derivative relative to y: Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1)))
微分の結果は分かりにくいが,これを簡約化する関数をパターンマッチで書くのは面白い,しかし,驚くほどトリッキーな問題なので,読者への課題としよう.
Traits
Scalaではスーパクラスから継承する以外に,複数のtraitからコードをインポートすることができる.
Javaプログラマには,trait をコードを含んだinterfaceだと考えると分かりやすいだろう. Scalaでは,あるクラスがtraitを継承すると,そのtraitのinterfaceを実装すると同時に,traitのコードを継承することになる.
traitの有効性を見るために,古典的な例である,順序づけられたオブジェクトを見てみよう.オブジェクトに順序をつけることは,たとえばソートなどをする際に役立つ.Javaでは,Comparable interfaceを実装することで実装する.ScalaではJavaよりもうすこしことができる.Comparableに似たようなtrait Ordを実装する.
オブジェクトの比較では6つの比較述語があり得る.しかしこれらをすべて定義するのは面倒だ.実際,6つのうち4つはのこりの2つの述語で表現できる.Scalaではこれを素直に実装することができる.
trait Ord { def < (that: Any): Boolean def <=(that: Any): Boolean = (this < that) || (this == that) def > (that: Any): Boolean = !(this <= that) def >=(that: Any): Boolean = !(this < that) }
このtraitは3つのメソッドのデフォルト実装を提供している.== と !=は書かれていないが,これらはすべてのオブジェクトに定義されているからである.
Any と書かれているのは,すべての型のスーパタイプとなる型である.IntやFloatのスーパタイプでもあるので,Java のObjectをさらに一般化したものだと言えるだろう.
比較可能なクラスを定義するには,== と < だけ定義して,上で定義したOrdをmixするだけでよい.グレゴリオ暦を示すDateというクラスを定義してみよう.Dateは整数で表される日,月,年で定義される.まずはこんな感じ.
class Date(y: Int, m: Int, d: Int) extends Ord { def year = y def month = m def day = d override def toString(): String = year + "-" + month + "-" + day
ここで重要なのはクラス名とパラメータの後の,extends Ord である.これでtrait Ordを継承している.
次に,equal メソッドを再定義する.デフォルトのequal メソッドは,オブジェクトのアイデンティティを比較してしまうので.
override def equals(that: Any): Boolean = that.isInstanceOf[Date] && { val o = that.asInstanceOf[Date] o.day == day && o.month == month && o.year == year }
ここでは,組み込みの isInstanceOf と asInstanceOf を用いている. isInstanceOf はJavaの instanceof 演算子に相当する.asInstanceOfはJavaのキャストに相当する.
最後に, < を定義する.ここでは組み込みメソッド error を用いている.これは,エラーメッセージつきのを例外を投げるメソッドである.
def <(that: Any): Boolean = { if (!that.isInstanceOf[Date]) error("cannot compare " + that + " and a Date") val o = that.asInstanceOf[Date] (year < o.year) || (year == o.year && (month < o.month || (month == o.month && day < o.day))) }
ジェネリックタイプ
Scalaでは,Javaの1.5から導入されたジェネリッククラスを利用することができる.
class Reference[T] { private var contents: T = _ def set(value: T) { contents = value } def get: T = contents }
初期値として「_」が用いられている.これはデフォルト値を意味する値で,数値型に対しては0,Booleanに対してはfalse, Unitに対しては (), オブジェクトに対してはnullとなる.
Refernce クラスを使う際には,セルに収める型を指定する必要がある.
object IntegerReference { def main(args: Array[String]) { val cell = new Reference[Int] cell.set(13) println("Reference contains the half of " + (cell.get * 2)) } }
この例から分かるように,取り出した値をキャストする必要はない.またこのReferenceにはInt以外の型の値をいれることはできない.
ここまで抄訳,以下感想.