第5回解説 プログラムの基本的な構造(main.scala)
Scalaのコードは通常オブジェクトと呼ばれる構造のなかで定義されます.
上はとても小さな例です.このmain.scalaのなかでひとつのオブジェクトが定義され,そのオブジェクトはmain
という名前を定義しています.
オブジェクトの要素
オブジェクトは,複数のものに名前を与えた上でひとまとめにする働きを持ちます.上の小さな例では,パズルの解法を実行して結果を表示するコードにmain
という名前を与えています.
前期の授業以来,このようなコードのことを関数と教わってきたと思います.オブジェクトとの関係において,その要素となっている関数のことをメソッドと呼びます.たとえば,上の例では,「Main
オブジェクトはmain
メソッドを提供します」とか,「main
はMain
オブジェクトのメソッドです」などという言葉づかいをします.
Mainオブジェクトが提供するmainメソッドのことを間にピリオドをいれて,Main.main
と表記します.上のプログラムのなかで,ピリオドを使っている個所がいくつかあります.
- 6行目の
List(Solution1, Solution2).foreach
- 7行目の
solution.name
- 8行目の
solution.solve
- 8行目の
solution.solve().foreach
がそれに該当します.ここで,Solution1とSolution2はそれぞれ solution1.scala と solution2.scala で定義されているオブジェクトの名前です.
最初と最後の場合はオブジェクトはList型のデータです.授業では,これまでListはリスト構造を表現し,それに対してパターンマッチができることを学んできました.実は,Listにはオブジェクトとしての正確も持ち合わせており,この例で使われているforeach
のほか,みなさんもよくご存知のlength
, map
, foldLeft
, zip
などのメソッドを提供しています.これらに代表されるリストオブジェクトが提供するについては後述します.
二番目と三番目の場合は,solutionという変数に束縛されたオブジェクトについて,それが提供するname
という名前のなにかとsolve
という名前のなにかを参照しています.ここで,solutionはsolution1.scalaで定義されているSolution1オブジェクト,あるいはsolution2.scala定義されているSolution2オブジェクトをさすので,solution.name
は実はSolution1.name
,あるいはSolution2.name
をさすことがわかります.
このことからobject定義されたオブジェクトが単なる宣言ではなく,データとして扱われることがわかります.たとえば,上のプログラムの6行目でList(Solution1, Solution2)
のようにリストを構成しています.ここで作成されたリストは,要素としてSolution1オブジェクトとSolution2オブジェクトを持っています.これまで,みなさんはList[Float], List[List[Int]]のようなリストを扱ってきたと思いますが,オブジェクトもFloatやList[Int]と同様にリストの要素にすることができます.
foreach
メソッドは,メソッドの左辺に与えられたリストの各要素を,引数に与えられた関数を用いて順次処理します.たとえば,以下の例は1
\((= 1^3)\), 8
\((= 2^3)\), …, 125
\((= 5^3)\)を画面に出力します.
def printTriple(x: Int) = println(x * x * x)
List(1, 2, 3, 4, 5).foreach(printTriple)
この例の場合,「1, 2, 3, 4, 5からなるリストの各要素xについて,x * x * x
を計算して画面に出力(println
)せよ」と読めばよいのです.
では,main.scalaの6行目に戻って,なにが書かれているか解読してみましょう.
無名関数
...さて,困りました.foreachの引数に与えられている((solution: Solution) =>
で始まるものがあります.これはなんでしょうか.
みなさんはdef宣言を用いて関数を定義して関数名を与えることは知っていますね.この方法で名前のついた関数を定義することができます.このように名前のある関数のほかに,Scalaでは名前のつかない関数(無名関数ともいいます)を定義することができます.一体全体,どうして名前のない関数など使いたいものかと思った人はすこし考えてみて下さい.プログラミングという作業では,オブジェクト,変数,関数,メソッド,型など,ありとあらゆるものに名前を与えます.気の効いた命名ができると,プログラムがわかりやすくなりますが,無神経な命名をしたプログラムは他のひとにとっては理解が困難です.ですので,プログラミング教育においては,わかりやすい命名をすることを厳しく求めます.実際にビジネスやオープンソースの場ではなおのこと適切な命名をすることが求めれます.
そろそろ命名するということのやっかいさをご理解いただけましたでしょうか.まだ,納得のいかない人は,引数に与えられた数\(x\)について,\(x^3 - 2x^2 + 5x - 3\)を計算した結果を出力する関数に誰もが納得するような名前をつけてみてごらんなさい.(ツライでしょ?)
こんなわけで,関数に名前をつけないですむというのも案外気楽なものなんです.Scalaの無名関数は以下のように記述します.
(x1: T1, x2: T2, ...) => expr(x1, x2, ...)
以下がScalaインタプリタで確認した例です.
scala> (x1: Int, x2: Int, x3: Int) => x1 + x2 + x3
res0: (Int, Int, Int) => Int = <function3>
scala> (s1: String, s2: String) => s1 + s2
res1: (String, String) => String = <function2>
scala> (x: Double, y: Double) => Math.sqrt(x * x + y * y)
res2: (Double, Double) => Double = <function2>
これらは,三つの整数の和,ふたつの文字列の連結,ベクトルの長さを与える無名関数のつもりです.
無名関数を利用するとき,つまり無名関数を引数に適用するときには無名関数を括弧で覆ったものを関数のつもりで使用します.
scala> ((x1: Int, x2: Int, x3: Int) => x1 + x2 + x3)(1, 2, 3)
res0: Int = 6
scala> ((s1: String, s2: String) => s1 + s2)("Hello ", "world!")
res1: String = Hello world!
scala> ((x: Double, y: Double) => Math.sqrt(x * x + y * y))(3, 4)
res2: Double = 5.0
無名関数が理解できたら仕上げに,main.scalaの6-9行目で利用されている無名関数を解読してみましょう.
(solution: Solution) => {
println(solution.name)
solution.solve().foreach(println)
}
ここで solution は,その直前のforeachから与えられ,リストの要素を走査する変数です.つまり,solutionはリストの各要素に該当します.ここで記述された無名関数は,Solution型のオブジェクトを走査して,その各要素のオブジェクトsolutionについて,名前(solution.name)を印字したのちに,solution.solveメソッドを実行した結果得られる解(後述のようにパズルの解をリストで与える)を印字します.
package宣言もオブジェクトの定義
目を最初のプログラムの1行目に移しましょう.
package puzzle
今回,取り扱っているほかの3つのプログラムも同様の宣言をしています.以下の表は,各ファイルのトップレベルで定義されている名前を列挙したものです.
ファイル名 | 定義された名前 |
---|---|
main.scala | object Main |
solution.scala | trait Solution |
solution1.scala | object Solution1 |
solution2.scala | object Solution2 |
これら4つのファイルが使用しているpackage宣言は,実は以下のようなobject宣言と見做すと理解しやすいでしょう.
object puzzle {
object Main { // main.scala内の記述
def main(...) { ... }
}
trait Solution { // solution.scala内の記述
def name: String
def solve(): List[List[Int]]
}
object Solution1 extends Solution { // solution1.scala内の記述
val name = "Solution 1"
def c(n: Int)(nums: List[Int]): Int = ...
def counts(...) ...
val countsOnPaper = ...
def satisfy(...) ...
val N = ...
def genCheck(...) ...
def solve() ...
}
object Solution2 extends Solution { // solution2.scala内の記述
val name = "Solution 2"
def genCheck(...) ...
}
}
この考え方に沿うと,solution1.scalaのなかで宣言されたsatisfyメソッドはpuzzle.Solution1.satisfy
で参照できることがわかります.
また,package宣言で同じパッケージに所属する要素は互いに同じスコープに存在することになるため,パッケージ名を省略して参照し合えることがわかります.たとえば,solution2.scalaのなかに以下の記述があります.
1 def genCheck(): List[List[Int]] = {
2 val range = List.range(0, 9)
3 for (x1 <- range; x2 <- range; x3 <- range; x4 <- range
4 if Solution1.satisfy(List(x1, x2, x3, x4)))
5 yield List(x1, x2, x3, x4)
6 }
この4行目にSolution1.satisfy
という参照があります.これはpuzzle.Solution1.satisfy
を参照しているのですが,solution1.scalaとsolution2.scalaがともにpuzzleパッケージに所属しているため,したがってSolution2.genCheck関数のスコープにSolution1.satisfyが含まれているために,パッケージ名が省略できるのです.
main(arguments: Array[String])メソッドについて
オブジェクトのメソッドのうち文字列配列を引数に取るmainという名称のメソッドは特別な役割が与えられています.このようなメソッドは,プログラムを実行したときに最初に起動するメソッドになります.
sbt
のなかで,run
コマンドを実行すると起動するのがこのようなメソッドです.場合によっては,複数のオブジェクトがそれぞれmainメソッドを提供していることもあるでしょう.たとえば,前述のファイル群に加えて,以下のように定義されたmain-test.scalaがあったとしましょう.
この場合,わたしたちの手元には以下の4つのmainメソッドがあることになります.
- puzzle.Main.main
- puzzle.Test1.main
- puzzle.Test2.main
- puzzle.Test3.main
この状況でsbtのなかでrunコマンドを実行してみましょう.
> run
[info] Compiling 1 Scala source to /Users/wakita/tmp/cs1f/lx05/scala-2.11/classes...
[warn] Multiple main classes detected. Run 'show discoveredMainClasses' to see the list
Multiple main classes detected, select one to run:
[1] maintest.Test1
[2] maintest.Test2
[3] puzzle.Main
Enter number:
実行可能なmainメソッドが三つもあるので,どれを実行したいのかと尋ねられているようです.あれっ,mainメソッドは4つあるのに,実行の候補には三つしかないのはなぜ?
さきほど書いたように,最初に起動するメソッドは名前がmain
である上に,文字列配列を引数にとらなくてはいけないのです.maintest.Test3.main
は引数はそもそも引数をとらないために,この条件に合致せず,最初に起動するメソッドとは見做されません.
では,さっそく最初の項目を選んで実行してみましょう.sbtからの問い合わせに対して1
を入力します.
Enter number: 1
[info] Running maintest.Test1
[info] Test1.main
[success] Total time: 357 s, completed 2015/11/09 22:01:06
maintest.Test1
を実行することができました.
runコマンドの親戚でrun-main
コマンドを利用すれば,目的とするmainメソッドを指定して実行することができます.run-main maintest.Test2
を実行してみましょう.
> run-main maintest.Test2
[info] Running maintest.Test2
[info] Test2.main
[success] Total time: 1 s, completed 2015/11/09 22:06:52
run-main
コマンドにオブジェクトの参照を指定した結果,sbtは三択の質問をせずに指定されたmainメソッドを実行してくれました.
ところで,三つのmainメソッドたちは文字列配列を受け取りますが,その引数はどこから与えるのでしょうか.実は,この文字列配列はrun-main
コマンドの引数がそのままmainメソッドに渡されるのです.たとえば,さきほどのrun-main
コマンドに続けて,五つの引数を渡してみたとします.
run-main maintest.Test2 A B C D E
ここで与えたA B C D E
はそれぞれScalaの文字列として扱われ,それら全部を配列としてまとめた上でmain
メソッドに渡されるのです.つまり,このmain
メソッドを起動するときに,裏では以下のような処理がなされていると思えばよいのです.
maintest.Test2.main(Array("A", "B", "C", "D", "E"))
この結果,以下のような出力が得られます.
> run-main maintest.Test2 A B C D E
[info] Running maintest.Test2 A B C D E
[info] Test2.main
[info] A
[info] B
[info] C
[info] D
[info] E
[success] Total time: 0 s, completed 2015/11/09 22:16:01
ところで,Scalaのプログラムをsbtを利用せずに実行することもできます.以下を見て下さい.
$ cd ~/tmp/cs1f/lx05/scala-2.11/classes
$ scala maintest.Test2 A B C D E
Test2.main
A
B
C
D
E
scala
コマンドを利用することで指定したオブジェクトのmainメソッドを起動しています.
ところで,scala
コマンドを起動するディレクトリは重要です.上の例では,~/tmp/cs1f/lx05/scala-2.11/classes
に移動してから,scala
コマンドを実行しています.このディレクトリのパスはどのように指定されているのでしょう.
この謎はbuild.sbt
ファイルの設定が答えてくれます.このファイルの最後に以下の指定があります.
// コンパイル結果を非標準の場所に設定
target := Path.userHome / "tmp" / "cs1f" / "lx05"
Path.userHome
はホームディレクトリを意味しています.この設定は要するに~/tmp/cs1f/lx05
にコンパイル結果を保存するように指定しています.Scalaのコンパイラは,このように指定されたディレクトリの下にScalaのバージョン名のディレクトリ/classes
を作成してそのなかにコンパイル結果を出力します.このclasses
ディレクトリの下にパッケージごとにコンパイル結果が整理されます.
前節へ 章へ 次節へ