Scalaでは、文は定義文しかない;普通の定義の他に、その定義の定義式を除いた形である宣言、またインポート指定やパッケージ定義も本質的には定義なのだから、これに含む。他はすべて、オブジェクトを項としメソッドを演算子とする「式」である。むろん、式中に文を混ぜることはできない(だから「文」として、それを別扱いしているわけである)。
パッケージ定義は、Javaと同じやり方だけでなく、パッケージ内定義を丸ごと、パッケージ名に続くブロック(このブロックを、Scalaでは特に「トップレベル」と呼ぶ;Java式の場合も、暗黙のトップレベルブロックが在ると見なされる)に入れることで、同一ファイル内に複数のパッケージを置ける。そもそもScalaでは、ファイル名とテンプレート名がJavaのように一致している必要は無く、同一ファイル内にいくつでもトップレベルのテンプレート定義を置ける;その上パッケージまでもが、やはりそんな風に同一ファイル内にいくつでも定義できてしまうわけだ。ただ、パブリックにソースをさらすような場合は、できるだけJavaの流儀に従った方がそのソースを読もうとする人々に対しより親切であることは言うまでもない。
インポートも基本的にはJavaのと同じだが、ワイルドカードは「*」ではなく「_」を使う。また、Scalaではさらに次のようなインポート指定も可能である。
import a.b.{A, B} // パッケージa.bのクラスAとBをインポート
import a.b.{C => X, D => _, _} // パッケージa.bのクラスCをXの名前で、Dを匿名で、
// 他残りはそのままインポート
匿名でインポートされた名前はプログラムからはアクセスできなくなる。また、(java.langに加えて)scalaパッケージ及びscala.Predefシングルトンは必ず暗黙にインポートされるので明示的なインポートは不要である。なお、Scalaでは三種のブロックレベルがあり、一つは上で触れたトップレベル、もう一つはテンプレートレベルで、それ以外のブロックとは可能なことが異なる。まず第一に、トップレベルでは、パッケージ定義、テンプレート定義とインポート指定しかできない。第二に、テンプレートレベルでは、パッケージ定義はできないが、その他の定義、宣言、式そしてインポートは可能だ。第三のその他のブロックでは、定義(テンプレート及び関数定義を含む)と式とインポートが可能である(つまり、宣言も不可になる)。
トップレベルではパッケージ定義も可能であること、すなわち、パッケージを入れ子定義できることに注意されたい(普通のJavaのような、その親パッケージ外で「親.子」のような命名での子パッケージ定義も可)。このために、インポートでのパッケージ指定はScalaでは相対的なものとなっている、つまり、そのインポート文が在るパッケージを基準に検索される。これを避けて絶対指定したい時は「_root_.」を頭に付ければ良い;この「_root_」はすべてのパッケージの上に在る暗黙の無名パッケージであって、インポート以外でこの名で参照することはできない。
Scalaではリテラルも(機構的にはメソッドも)含めて、すべてはオブジェクトである。だから、代入は値か参照か、という区別は無意味だ、つまり、すべては意味論的には参照しかあり得ない(実際にはコンパイル時に値代入や値渡しに絞れる時はそれに最適化されるのだけれども)。すべての識別子は、ゆえに、何かへのエイリアスである、そして、それが何のエイリアスであるか定めることを、「識別子を束縛する」と言う。この束縛は一度きりであり、すなわちひとたび為された束縛は二度と変更できない。
Scalaは、そんなわけで、四種類の束縛で既存のプログラミングを根底から捉え直す:値への束縛(val)、型への束縛(var)、式への束縛(def)、そして可能性の束縛(type)、だ。
「val」は、定義式の結果としての値(すなわちScalaではインスタンス)へ識別子を束縛する;これは慣習的な分かり易さから「定数」と呼ばれるけれども、既に書いたように、束縛であって代入では無いのだから、厳密には定数では無い。例えば、「val a = Array(1, 2, 3)」はその配列自体への束縛に過ぎず、その要素の書き換えを禁ずるものでは無い。
「var」は、定義式の結果としての値の型へ識別子を束縛する。これは、その型の形ののぞき窓のようなものだ。窓の形はもはや二度と変えられないが、窓自体は、その形への適合性をキーにどこにでも動かせる。それで、慣習的分かり易さからこれは「変数」と呼ばれるけれども、その実体は、変数がしばしば値をそこへ放り込める箱と比喩されるのとは逆に、値のある場所へと自ら出向きそれを読み取る「探索者」なわけで、やはり既存の変数概念とは一線を画するものではある。
「def」は、定義式それ自体へ識別子を束縛する。すなわち、これで定義された識別子は、呼ばれたところにその定義式自体を埋め込み立ち去るのだ;これを(より汎用化された意味で)遅延評価と呼ぶならば、「val」のそれは事前評価と呼び得よう(ただし、「val」も前に「lazy」を付けることで、一度だけ遅延評価させることはできる)。
「type」は、定義式によって示される「集合範囲」へと識別子を束縛する。テンプレートが、厳密には値の「性質」を定義するのに対し、型は厳密にはその「可能な範囲」を定義する。ただ、テンプレート名と型名が(ほとんど)一致する間は、私自身がこれまでしばしばそうしたように、型名すなわちテンプレート名と見なしても特に不都合は無いし、少なくとも「Javaと同等のことができるようになる」までは、そんなわけで「型」について厳密に解説する必要は無い(だから、私はそれをこの解説諸ページの、重要文法解説の最後に置いた;要するに、それまではこの「type」はテンプレート名のエイリアスのようなものだという理解だけで十分だ、と言うことである)。
「val」と「var」による定義では(「def」は関数説明のページで解説)、左辺に、普通の識別子の他にパターンも使える(前者は大文字で始まる識別子や記号識別子も束縛するが、後者は変数識別子のみ束縛可能であることに注意)。それで、複雑めな定義は次のような感じになる。
val X, y1::y2, z = 1::2::3::4::5::Nil // XとzはList(1, 2, 3, 4, 5) // y1は1 // y2はList(2, 3, 4, 5)
この例のように、「val」及び「var」定義左辺では「,」で複数の識別子やパターンを列挙し一度に定義できるわけだが、その各項は普通の識別子かパターンかのいずれかでなければならない。この例では型の指定は無理だが、もっと単純な定義では
val x, y: Char = 34 // xとyを二重引用符文字に束縛
のような形で型指定できる。ただし、定義式から結果の型が明らかで、かつその型で別に構わない場合は、型指定は(上の例のように)省略して良い(型が無いのではなく、Scala側で型を推定し決めてくれる)。なお、パターンが左辺に使えるのは定義の時だけで、宣言の場合はもちろん不可である。
式評価の優先順位は次の通り:型推定>リテラル>null>識別子>thisとsuper>関数呼び出し>メソッドを返す無名関数>型引数>タプル>new>ブロック式>前置メソッド>引数のあるメソッド>引数の無い(すなわち手続き)メソッド>代入演算子の演算部分>帰属型>帰属注釈>代入>if式>while式>do式>for式>return式>throw式>try式。
型周り、クラス周り、関数周りと注釈周りは後の諸ページで解説するとして今は省く。帰属型は、要するに式の型を、上の定義と同様のやり方で明示できる、ということだ。Scalaでは、既にも述べたように、演算子もメソッドである。前置メソッドとは要するに前置演算子のことで、Scalaでは「-」,「+」,「~」と「!」の四つだけが前置可能で、実際のメソッド名は「unary_+」のような形で定義する。上で同一優先順位にあるメソッド同士は、またさらに別の優先順位に従う。
メソッド名の評価優先順位は次の通り:右に列挙以外の記号>「*」と「/」と「%」>「+」と「-」>「:」>「=」と「!」>「<」と「>」>「&」>「^」>「|」>文字。
メソッド名の最初の文字が上のいずれであるかでその優先順位が決まる。上も含めて、一般にScalaでは、同じ優先順位のものが続く場合その各々の評価は左側から、つまり横書きの文を普通に読む順で為される(つまり、「a+b+c」は「(a+b)+c」と見なされる)のだが、識別子の最後が「:」で終わる場合だけ「右側から」になる(「1+:2+:3」は「1+:(2+:3)」と見なされる。またこの場合、「a :: b」は必然的に「b.::(a)」でなければならないことに注意)。なお、同じ優先順位のものの並びの評価方向はすべて同一でなければならない。
数値型、Boolean型、文字型、文字列型は、「++」及び「--」が無い以外、Javaとまったく同じ演算子が使える。ただ、優先順位はあくまで上のルールに従うのでちょっと注意が必要だ:関数以外の配列を含む文字名メソッド呼び出しは演算子より後に評価されること、newが前置演算子に先行すること、「==」と「!=」がその他の比較やシフト演算子に先行すること、同じ記号から成るビット演算子と論理演算子が同位になること。最初の二つは、演算子もメソッドであるおかげで実害は無いけれども、後の二つ(特に「==」「!=」とその他の比較演算子)は、人によっては慣れるまでちょっと苦労するかもしれない。キャストは「asInstanceOf[型名]」、Javaの「instanceof」は「isInstanceOf[型名]」で代替される。三項演算子も、ifが式なのでそれで問題無く置き換え可能だ。「++」と「--」は、もしどうしたくても使いたければ、「++x」は「{x += 1; x}」で、「x++」は「{val y = x; x += 1; y}」で置き換えることになる(詳細は後のブロック式のところで解説)。なお、Scalaでは「==」は常に「equals」のエイリアスであり、AnyRef(Javaで言うところのObject型;実際、Javaに対してはAnyRefはObjectとして渡される)ではそれはインスタンスの同じであることを意味する「eq」(ちなみにこの逆は「ne」)のエイリアスである;これは要するに、「equals」の実装次第で「==」の意味が変わることを意味する。既に挙げたAnyRef系の型では、配列だけが「==」はインスタンス同一性の意味であり(内容同一性はdeepEqualsまたはsameElementsメソッド)、文字列・リスト・セット及びマップでは「==」は内容同一性の意味である(インスタンス同一性はeq。なお、リストは自前(継承)でequalsを再定義してないが、どうやら「::」ケースクラスをどういうルートでか経てそちらのケースクラスなequalsが使われる形になっているらしい)。
上で挙げたJavaの基本型は演算や代入時に暗黙の型変換をするが、Scalaでももちろんその通りに変換される。ただし、これはScala自体の機能では無い。Scala自体が提供するのは暗黙の型変換を可能にする「ビュー」と言う機能だけで、Javaと同じ暗黙型変換そのものは、このビューを使って、インポートのところで言及したscala.Predefシングルトン内で初めて定義されている。つまり、どうしてもそれがイヤな人は、これを差し替えて付属ライブラリを再構築すればデフォルトな暗黙の型変換を無効化することも不可能ではない、ということだ(付属ライブラリはそれが有ることを前提としているので、再構築は相当大変な作業になると予想されるけれども)。一方逆に、そのPredefはまたJavaの基本型をより便利にするユーティリティクラスへの暗黙型変換も定義していて、このおかげでScalaのそれらは他にも様々な(もっぱら文字系名だが)演算子も使えるようにもなっている(特に有益なものは、追って紹介されるであろう)。あと、このPredefシングルトンはJavaのそれ自体の基本型ラッパークラスへのビューも持っているので、それらのメソッドもデフォルトで利用可能である(それも含めて、ScalaではJavaのクラス、フィールド、メソッド及びインターフェースはあたかもScala自身のそれであるかのように継承、呼び出し、参照及びインスタンス化が可能だ)。
代入すなわち単一の「=」は、予約語であることからも分かるように、普通の演算子とは扱いが異なる;Scalaの代入式自体の値は、必ずUnit型である。
そもそも、定義のところでも述べたように、これを「代入」と呼ぶこと自体が厳密には正しく無い;厳密にはそれは「var」識別子の参照を右辺の結果のインスタンスへと移動させること、であって、その識別子で呼ばれる「箱」(すなわちメモリ領域)にそれを「代入」すなわちコピーすること、ではまったく無い。インスタンス参照を未定義な外部へと漏らすことは、その識別子とそのインスタンスとの同期性がいつどこで破壊されるか知れない、という害だけしかなく、値の連続コピーという利も無いのだから、Scalaの代入式の結果がUnit型であることは、むしろ当然である。この場合も、どうしてもそうしたければ「{x = 1; x}」という形でできるが、また同時にこの表現自体が今言った「危うさ」を如実に表しているとも言えるだろう。
Scalaでも代入演算子は使えるが、特別にその形でメソッド定義していなければ、それはすべて「x = x 演算子 値」の簡略表現と見なされ、実際内部的にもその形に直される。これが特にJavaとは異なる状態を生成するとは思われないが、このゆえにScala独自の演算子にもこの形が普通に使える(「list ::= 3」等;ただし上記したように「:」は右から評価するので、結果は「3::list」と分かりづらいものにはなるが)。
ブロック式は、Javaのブロックと同じに「{}」(波括弧)で囲まれた、複文を含め得るものを言う。違うのはScalaではこれも式であることで、「}」の直前の式の値がそのままブロック式の値になる(直前が「;」だと値はUnit型になる;この場合改行にはその意味は無い)。ブロック内では、既に述べたように、宣言は不可だが定義と式の他にもインポートとテンプレート定義が可能である;最初の定義の中には、当然ながら関数定義も含まれることに注意されたい。トップレベルブロックとテンプレートブロック以外はすべてこのブロックで、関数やメソッド本体としてのブロックも例外では無く、つまりは、それらの内部でもテンプレートや関数が定義できる、ということである。なお、同じブロック式内で同一の識別子は一度しか定義できない。また、前方参照は、間に「val」または「var」定義が無い場合のみ可能である(それらの定義式は実行時に評価されるので、それより後ろにある該当定義の値を事前には予測不可能にしてしまうため)。
Scalaには三つの名前空間、すなわち普通名空間と型名空間、そして暗黙名空間とがあり(最後のは私による定義命名で、本家の言語仕様では直接言及されてはいないが)、ブロックは、トップレベル及びテンプレートのも含めて、このうち最初の普通名空間のスコープ単位である(要するに、名前空間に関しては、普通名空間だけがJavaの名前空間に(ほぼ)等しい)。
if式は、真偽式の結果として評価されるのが文では無く式である他は、Javaのif文と同じである(ただ、elseの前に「;」が1つ在っても良い)。if式の値は、elseがある場合はtrueで評価される式とfalseで評価される式の値の型の、より祖型の方になり、elseが無ければ必ずUnit型になる。
while式とdo式もif式と同じく、Javaの該当文のループ毎に評価される文が式へと変わっただけもの(ただここでも、do式の方の最後のwhileの前には「;」が一つ在っても良い)。こちらはしかし、if式とは異なり結果は必ずUnit型になる。次のfor式もそうだが、Scalaを学び始めたJava経験者が一番戸惑うのが、Scalaのループにはbreakとcontinue(とその飛び先としてのラベル文)が無い、ということのようだ。どうしてもそういうことをしたければ、ループの真偽式にBooleanを値とするブロック式を投入することになるのだろうが、
while ({
print("Give me a number: ")
val i = readInt
if (i < 5)
{println("Too small!"); true}
else
{println("Enough!"); false}
}) ()
やはり分かりずらいことは否めまい(ちなみに、print系メソッドで実引数の括弧が省けないのは、省くと実引数が後置メソッドと解釈されてしまうからである。どうしても省略したい時は、print系メソッドの前にその定義実体である「Console」を置けば一応括弧を省けるようになるが、いちいちそれを置くよりは括弧を付けた方が楽だろうと思う);やはり作者等が推薦するように、関数分けするか再帰関数でロジックを組めるクセを付けるよう心がけるのが無難かと思う(後者は後々、その副作用の無さ(再帰関数で上手に組めれば引数以外の変数が不要になる)がなにかと助けになるような気もするし)。
match式はJavaでいうところのswitch文に相当するものだが、上までと違って表現等が異なる。
val list = {
print("Give me a number: ")
readLine.toList
}
println(
list match {
case x::y if y.length > 2 =>
println("Too long!")
x::y.head::y.tail.head::Nil
case x::y if y.length == 2 =>
println("This! So I reverse it!")
y.last::y.head::x::Nil
case _ =>
println("Too short!")
1::2::3::Nil
}
)
Javaではswitchの後ろの丸括弧の中にある「マッチ元」は、match式ではmatchの前の「式」である。caseと「=>」の間はパターンで、これはマッチ元の値が可能な型でなければならない。最初の二つのcaseの「if y.length > 2」や「if y.length == 2」を「ガード」と呼び、この条件に当てはまらないマッチを無視させるものだ。「=>」の後ろから次のcaseまでの間はブロックだが、ブロック式と異なり波括弧では囲まない。上から順に、最初にマッチしたケースパターンのブロックだけが評価される。match式の値の型は、その各々のマッチ先ブロックの値のうちの最も祖型なものになる。また、マッチするものが一つも無い場合はMatchError例外が投げられる。
for式もまた、Javaのそれとは結構異なる。
println(
for {
i <- 0 to 20 if i % 2 == 0
val j = i * 3 - 2
if j % 2 == 0
} yield (i, j)
)
「パターン <- 式」は、ループ毎に式の値をパターンの変数識別子(ここでは単純な普通識別子は不可)に束縛する(厳密には少し違う;下の解説群を参照のこと)。式は、その結果型がfilterメソッドを持ってさえいれば何でも良い。なお、この式は「ループの前」に一度だけ評価されるのだが、「.projection」を後ろに付けることで「各ループごと」の評価へ変更できる(つまり、「.projection」はその型と同等な遅延型を返すキャストメソッドである)。
上のforの一行目のガードを除く部分は、Javaでの「int i = 0; i <= 20; i++」と同じ意味だ。「to」がその逐次増大を生成するメソッドで、「until」だと「i < 20」すなわち指定右辺項は含まない;これらはscala.runtime.RichIntで定義されており、Predef定義のビューでInt型からでも呼び出し可能になっている。そのAPIを見れば分かるように、結果はscala.Range.Inclusive(toの場合)及びscala.Range(untilの場合)型で、これらがAPIでその下の方の祖型クラスからfilterメソッドを継承していることが分かる(後、map、flatmapとforeachメソッドが継承されていることもついでに確認しておきたい;for式とは、これら四つのメソッドの組み合わせを実体とする、完全な糖衣構文なのである(どう変換されるか、は、-Xprint:parseオプションで確認可能だ;ただし、高階関数についての理解を要する))。また、Range型でbyメソッドが確認できるが、これは増分を指定するもので、これから「0 to 20 by 2」(0、2、4...16、18、20)や「5 to 1 by -1」(5、4、3、2、1)といった生成も可能となる。
二行目、この「val」定義は、ループ毎に定義式が評価される(なお、一行目のiも暗黙の「val」指定なので、ループ内での変更は不可である)。三行目は、これもガードだが、一行目のものがiが束縛されるべき値へのガードなのに対し、こちらはループするかしないか、へのガードである;一行目のようなもの(生成子と呼ぶ)は複数指定でき、すなわち多重ループとなるわけだが、それらを一括してガードしたい場合に特に有効だ。
四行目の「yield」は、その次の式の値を生成子の型(上の例ではRangeFMFMとなるが、これはmapで結果的に起動されるscala.seq.Projection.MapProjectionが、filter使用ならF、mapならM(その他flatMapならG、takeWhileならTW)を、クラス名を返すscala.CollectionのtoStrongに追加する仕組みになっているからで、要するにfilterとmapが二度繰り返されて(「val」定義がもう一つのmap適用で、単独ガードがもう一つのfilter適用)生成されたRange型、という意味である)でまとめたものをfor式の値とする指定で、これが無ければfor式の値はUnit型になる。「yield」がある場合はmapメソッド(生成子が複数の時はflatmapメソッド)を、無い場合はforeachメソッド(こちらは生成子が複数でも同じforeachメソッド)を使ったループ(生成子がIterable型の子であればループなfilter適用をサポートする;無論、Range型もその子型である)が実際には展開されることになる。
このようにfor式は完全な糖衣構文であり、従ってfilter、map、flatmap及びforeachメソッドの実装次第では、以上と異なる動作やオプション項等が可能となることに注意されたい。
なお、上では生成子等を含む部分を囲むのに波括弧を使ったが、ここは丸括弧でもよく、ただしその場合は上で改行分けしているところを「;」で区切らねばならない、という違いがある。
try式は、catch部以外はJavaと同じである。
try {
throw new NotDefinedError("Please anybody write a code here.")
}
catch {
case e: NotDefinedError =>
println("Hum, \"" + e.getMessage + "\"?")
print("Don't throw anything ")
case e: RuntimeException =>
print(e.getStackTraceString)
print("those have opposed ")
}
finally println("before trying by yourself!")
throw式はJavaのと同じ(だがやっぱり式)で、throw式自体の型はscala.Nothingである。scalaの例外やエラーはjava.lang.Throwableを必ず継承し、またその子ならJavaのも含めてすべて認識できる。Predefで定義されているもの、すなわちインポート無しで使えるのは、Javaからは:java.langのThrowable、 Error、 Exception、 ArrayIndexOutOfBoundsException、 ClassCastException、 IllegalArgumentException、 IndexOutOfBoundsException、 NullPointerException、 NumberFormatException、 RuntimeException、 StringIndexOutOfBoundsException、 UnsupportedOperationException、 java.utilのNoSuchElementException。 Scala自身のは:scalaのMatchError、 NotDefinedError、 UninitializedError。 その他、Scala自身は以下のものも持っている:scala.runtimeのNonLocalReturnException、 scala.util.regexpのSyntaxError、 scala.xml.parsingのFatalError、 scala.xmlのMalformedAttributeException、 scala.xml.PrettyPrinterのBrokenException、 scala.xml.dtdのValidationException、 そしてscala.xml.includeのCircularIncludeException、 UnavailableResourceException、 XIncludeException。
try式の値の型は、tryブロックとcatchの各ケースパターンのブロックの最も祖型なもので、いずれか最後まで評価されたものがその値になる。finallyの式(ここだけブロックでなくて良い)の値はUnit型だが、tryブロックが無事に評価された場合は、finallyが評価されてもtry式の値はあくまでtryブロックのそれになる。
例外やエラークラスはScalaのでもunapplyメソッドは持っていないので、上のように「変数識別子: 型」の「型」の方に置く必要がある(変数識別子が不要なら「_ : 型」でも良い;これは無論、catch以外のパターンでも使える方法である)。getMessageはJavaのそれ、getStackTraceStringはscala.runtime.RichExceptionのそれである(scala.runtimeのこれら「Rich型名」は、すべてPredefのビューで暗黙にその型名のメソッドとして使用可能)。なお、catchのケースパターンは、マッチするものが無くてもその投げられた例外やエラーを、自身を内包する他のtry式へとスルーするだけで、自身がさらにMatchErrorを投げることは無い。なおまた、try式が無ければScalaが暗黙に必要なthrowsを生成するので、Javaのthrowsを持つメソッドを使う時にも別にtry式は無くても良い。
Javaのsynchronized文すなわち「synchronized (式1){文}」は、Scalaでは「式1.synchronized{式2}」と書く(式2はJavaの方の文に等しいScala式);scala.AnyRefで定義されているメソッドである。