プログラムを作っていると、以前作った関数と同じ処理が必要になる場合があります。いちばんてっとり早い方法はソースファイルからその関数をコピーすることですが、賢明な方法とはいえません。このような場合、自分で作成した関数をライブラリとしてまとめておくと便利です。
ライブラリの作成で問題になるのが「名前の衝突」です。複数のライブラリを使うときに、同じ名前の関数や変数が存在すると、そのライブラリは正常に動作しないでしょう。この問題は「パッケージ (package)」を使うと解決することができます。
Scala は Java と同様に、パッケージの中に複数のクラス (class) やオブジェクト (object) を定義することができます。パッケージは package 文で宣言します。package 文がない場合は無名のパッケージとして扱われます。package 文の構文を示します。
package 名前 { ... }
Java と違い、Scala は一つのファイルに複数のパッケージを記述することができます。
簡単な例を示しましょう。
リスト : パッケージの使用例 (1) package foo { class Foo { val x = 1 } }
ファイル foo.scala に package foo を宣言し、その中でクラス Foo を定義します。このプログラムは package の { } を省略して、次のように記述することができます。
リスト : パッケージの使用例 (2) package foo class Foo { val x = 1 }
scalac で foo.scala をコンパイルすると、サブディレクトリ foo が作成され、その中にコンパイルされた Foo.class が格納されます。クラス Foo を使用するときは、パッケージ名を前に付けて foo.Foo とします。Java と同様に、区切り記号にはドット ( . ) を使います。これを完全修飾名といいます。import 文を使うと、Foo だけで使用することができます。これはあとで説明します。
パッケージは入れ子にすることができます。
リスト : パッケージの入れ子 (1) package foo { class Foo { val x = 1 } package bar { class Bar { val y = 2 } } }
このプログラムも package の { } を省略して、次のように記述してもかまいません。
リスト : パッケージの入れ子 (2) package foo class Foo { val x = 1 } package bar class Bar { val y = 2 }
このプログラムをコンパイルすると、サブディレクトリ foo の中にサブディレクトリ bar が作成され、その中に Bar.class が格納されます。クラス Foo のアクセスは foo.Foo で、クラス Bar のアクセスは foo.bar.Bar となります。
Scala は一つのファイルに複数のパッケージを記述することができます。また、次のようにパッケージ名をドットで区切ることもできます。
リスト : 複数のパッケージ (sample1800.scala) package foo { class Foo { val x = 1 } package bar { class Bar { val y = 2 } } } package baz { class Baz { val z = 3 } } package baz.oops { class Oops { val mes = "oops" } }
$ scalac sample1800.scala $ ls -R foo foo: Foo.class Foo.tasty bar foo/bar: Bar.class Bar.tasty $ ls -R baz baz: Baz.class Baz.tasty oops baz/oops: Oops.class Oops.tasty
Baz.class はサブディレクトリ baz の中に、Oops.class は baz の中のサブディレクトリ oops の中に格納されます。
パッケージ内のクラスは import 文を使うと簡単に使用できるようになります。次の例を見てください。
$ scala -classpath . Welcome to Scala 3.3.4 (21.0.5, Java OpenJDK 64-Bit Server VM). Type in expressions for evaluation. Or try :help. scala> new foo.Foo val res0: foo.Foo = foo.Foo@61ab6521 scala> import foo.Foo scala> new Foo val res1: foo.Foo = foo.Foo@be6d228 scala> import foo.bar._ scala> new Bar val res2: foo.bar.Bar = foo.bar.Bar@6e93b0e7 scala> new foo.bar.Bar val res3: foo.bar.Bar = foo.bar.Bar@40f9f60d
コマンド scala のオプション -classpath は、ユーザが作成した class ファイルを探すパスを指定します。デフォルトではカレントディレクトリに設定されているはずですが、M.Hiroi が使用している Scala ver3.3.4 では、-classpath . の指定が必要でした。
クラス Foo は foo.Foo で使用することができます。import foo.Foo とすると、REPL の名前空間に Foo がインポートされて、完全修飾名ではなく Foo だけで使用することができます。アンダーバー ( _ ) を使うと、パッケージに定義されているクラスをすべてインポートすることができます。これは Java の * と同じ機能です。import foo.bar._ でパッケージ foo.bar で定義されているクラスやオブジェクトをすべてインポートすることができます。
パッケージの中で特定のクラスだけをインポートすることもできます。
import パッケージ名.{クラス1, クラス2, ... }
{ } の中でインポートするクラスを指定します。簡単な例を示しましょう。
リスト : パッケージ foo1 package foo1 class Foo { val x = 1 } class Bar { val y = 2 } class Baz { val z = 3 }
scala> import foo1.{Foo, Bar} scala> new Foo val res0: foo1.Foo = foo1.Foo@6598caab scala> new Bar val res1: foo1.Bar = foo1.Bar@be6d228 scala> new Baz -- [E006] Not Found Error: ------------------------------------ 1 |new Baz | ^^^ | Not found: type Baz - did you mean Bar? | | longer explanation available when compiling with `-explain` 1 error found
インポートするとき、クラスに別名を付けることもできます。
import パッケージ名.{クラス名1 => 別名, ...}
簡単な例を示しましょう。
scala> import foo1.{Foo => Oops} scala> new Oops val res0: foo1.Foo = foo1.Foo@2f86f9cf scala> new foo1.Foo val res1: foo1.Foo = foo1.Foo@3df77dfa scala> new Foo -- [E006] Not Found Error: ------------------------------------- 1 |new Foo | ^^^ | Not found: type Foo | | longer explanation available when compiling with `-explain` 1 error found
また、次のようにアンダーバーと組み合わせて、残りのクラスをすべてインポートすることもできます。
scala> import foo1.{Foo => Oops, _} scala> new Oops val res0: foo1.Foo = foo1.Foo@30c3ae63 scala> new Bar val res1: foo1.Bar = foo1.Bar@35bfa1bb scala> new Baz val res2: foo1.Baz = foo1.Baz@6d294ddc
シングルトンオブジェクトをインポートすることもできます。簡単な例を示しましょう。
scala> object Foo { | var x = 1 | def getX: Int = x | def putX(a: Int): Unit = x = a | } // defined object Foo scala> Foo.x val res0: Int = 1 scala> Foo.getX val res1: Int = 1 scala> Foo.putX(10) scala> import Foo._ scala> x val res3: Int = 10 scala> getX val res4: Int = 10 scala> putX(100) scala> x val res6: Int = 100
アンダーバーを使って、シングルトンオブジェクトに定義されている変数や関数をすべてインポートすることができます。また、インポートする関数や変数を指定することもできます。
scala> object Bar { | val x = 10 | val y = 20 | def getX: Int = x | def getY: Int = y | } // defined object Bar scala> import Bar.{getX => getBX, getY} scala> getBX val res7: Int = 10 scala> getY val res8: Int = 20
Bar のメソッド getX は名前を getBX に変えてインポートし、メソッド getY はそのままインポートしています。
ちなみに、print や prntln はシングルトンオブジェクト Predef に定義されています。Scala は Predef を含め 3 つのパッケージ (やオブジェクト) を暗黙のうちにインポートしています。
import java.lang._ // Java の基本クラス import scala._ // scala の基本クラス import Predef._ // Predef オブジェクト
パッケージオブジェクトは、パッケージをシングルトンオブジェクトのように扱うための機能です。
package object 名前 { ... }
package object の後ろに名前を指定し、{ ... } の中にフィールド変数やメソッド、クラスを定義します。同じパッケージ内でクラスを定義する場合、パッケージオブジェクトの変数やメソッドは、パッケージ名をつけなくてもアクセスすることができます。
簡単な例を示しましょう。
リスト : パッケージオブジェクト package pack package object foo { val x = 1 def getX: Int = x class Foo { val y = 2 def foo: Int = x + y } }
pack.foo はパッケージオブジェクトです。クラス Foo は pack.foo に定義されているので、フィールド変数 x は修飾名を付けなくてもアクセスすることができます。上記プログラムは、次のように 2 つのファイルに分割することもできます。
リスト : パッケージオブジェクト (2) // // pack.scala // package pack package object foo { val x = 1 def getX: Int = x } // // packFoo.scala // package pack.foo { class Foo { val y = 2 def foo: Int = x + y } }
クラス Foo はパッケージオブジェクト foo.bar の中で定義されているので、異なるファイルに分割されていても、完全修飾名なしでフィールド変数 x を参照することができます。
パッケージオブジェクトはパッケージと同様に import 文でインポートすることができます。
$ scalac pack.scala $ scalac packFoo.scala $ scala -classpath . Welcome to Scala 3.3.4 (21.0.5, Java OpenJDK 64-Bit Server VM). Type in expressions for evaluation. Or try :help. scala> import pack.foo._ scala> x val res0: Int = 1 scala> getX val res1: Int = 1 scala> val a = new Foo val a: pack.foo.Foo = pack.foo.Foo@73dbe25 scala> a.foo val res2: Int = 3
ところで、Scala 3 ではパッケージオブジェクトと同様のことを、package のトップレベルで行うことができるようになりました。つまり、変数、メソッド、型エイリアスなどの定義をトップレベルで行うことができます。このため、パッケージオブジェクトは将来的には廃止される予定です。
パッケージオブジェクトを使わずに pack.scala を書き直すと次のようになります。
リスト : パッケージのトップレベル定義 // // pack.scala // package pack val x = 1 def getX: Int = x // // packFoo.scala // package pack.foo { class Foo { val y = 2 def foo: Int = x + y } }
実行結果は同じなので割愛させていただきます。
Scala のアクセス制御は、private, protected, 指定無し (public) の 3 通りの方法がありますが、このほかにも「スコープ限定子」という機能があります。これは private, protected のあとに角カッコで公開するクラス (オブジェクト) やパッケージを指定する機能です。
簡単な例を示しましょう。
リスト : スコープ限定子 package foo { class Foo { private[foo] var x = 1 } class Bar { var y = 2 def getX(a: Foo): Int = a.x } } object sample1801 { def main(args: Array[String]): Unit = { val a = new foo.Foo val b = new foo.Bar println(b.getX(a)) } }
$ scalac sample1801.scala $ scala sample1801 1
private で指定されたクラス Foo のフィールド変数 x は、他のクラスからアクセスすることはできませんが、private[foo] と指定すると、パッケージ foo に定義されているクラスであればアクセスすることができます。したがって、Bar のメソッド getX は Foo のフィールド変数 x を参照することができます。
なお、Scala 2 では private[this] というように this を指定をすると、同じインスタンスしかアクセスすることができなくなりますが、Scala 3 以降では非推奨になったようです。説明は割愛させていただきます。
パッケージと直接関係はありませんが、もう一つアクセス制御の機能を紹介しましょう。class の前に sealed を付けると、そのクラスは「シールドクラス」に設定されます。シールドクラスと同一ファイルにあるクラスは、シールドクラスを継承することができますが、別ファイルのクラスはシールドクラスを継承することができません。
簡単な例を示しましょう。
リスト : 図形のクラス (figure.scala) package figure sealed abstract class Figure { def kindOf: String } case class Triangle() extends Figure { def kindOf: String = "Triangle" } case class Rectangle() extends Figure { def kindOf: String = "Rectangle" } case class Circle() extends Figure { def kindOf: String = "Circle" }
抽象クラス Figure はシールドクラスなので、ファイル figure.scala 内では Figure を継承した case クラスを作ることができます。次のように、パターンマッチングを使って図形の種類ごとに個数を数えるプログラムを作ることができます。
リスト : 図形をカウントする (sample1803.scala) import figure._ object sample1803 { def countFigure(xs: List[Figure]): List[Int] = { var a = 0 var b = 0 var c = 0 for (x <- xs) { x match { case Triangle() => a += 1 case Rectangle() => b += 1 // case Circle() => c += 1 warning!! } } List(a, b, c) } def main(args: Array[String]): Unit = { println(countFigure(List(new Circle(), new Rectangle(), new Triangle(), new Rectangle(), new Triangle(), new Rectangle()))) } }
ここで、パターンマッチングに漏れがあることに注意してください。シールドクラスの場合、パターンマッチングに漏れがあると警告が表示されます。
$ scalac figure.scala $ scalac sample1803.scala -- [E029] Pattern Match Exhaustivity Warning: sample1803.scala:9:6 ------------ 9 | x match { | ^ | match may not be exhaustive. | | It would fail on pattern case: figure.Circle() | | longer explanation available when compiling with `-explain` 1 warning found $ scala sample1803 scala.MatchError: Circle() (of class figure.Circle) ... 省略 ...
警告を無視して実行すると、実行時に MatchError が送出されます。コメントを有効にして、パターンマッチングに漏れをなくすと正常に実行することができます。
$ scalac sample1803.scala $ scala sample1803 List(2, 3, 1)
また、次のように別ファイルで Figure を継承した case class を作るとコンパイルエラーになります。
リスト : 図形をカウントする (sample1804.scala) import figure._ case class Pentagon() extends Figure { def kindOf: String = "Pentagon" } object sample1804 { def countFigure(xs: List[Figure]): List[Int] = { var a = 0 var b = 0 var c = 0 var d = 0 for (x <- xs) { x match { case Triangle() => a += 1 case Rectangle() => b += 1 case Circle() => c += 1 case Pentagon() => d += 1 } } List(a, b, c, d) } def main(args: Array[String]): Unit = { println(countFigure(List(new Circle(), new Rectangle(), new Triangle(), new Rectangle(), new Triangle(), new Rectangle()))) } }
$ scalac sample1804.scala -- [E112] Syntax Error: sample1804.scala:3:11 ---------------------------- 3 |case class Pentagon() extends Figure { | ^ | Cannot extend sealed class Figure in a different source file | | longer explanation available when compiling with `-explain` 1 error found