Scalaのコードは通常オブジェクトと呼ばれる構造のなかで定義されます.

上はとても小さな例です.このmain.scalaのなかでひとつのオブジェクトが定義され,そのオブジェクトはmainという名前を定義しています.

オブジェクトの要素

オブジェクトは,複数のものに名前を与えた上でひとまとめにする働きを持ちます.上の小さな例では,パズルの解法を実行して結果を表示するコードにmainという名前を与えています.

前期の授業以来,このようなコードのことを関数と教わってきたと思います.オブジェクトとの関係において,その要素となっている関数のことをメソッドと呼びます.たとえば,上の例では,「Main オブジェクトはmainメソッドを提供します」とか,「mainMain オブジェクトのメソッドです」などという言葉づかいをします.

Mainオブジェクトが提供するmainメソッドのことを間にピリオドをいれて,Main.mainと表記します.上のプログラムのなかで,ピリオドを使っている個所がいくつかあります.

  1. 6行目の List(Solution1, Solution2).foreach
  2. 7行目の solution.name
  3. 8行目の solution.solve
  4. 8行目の solution.solve().foreach

がそれに該当します.ここで,Solution1とSolution2はそれぞれ solution1.scalasolution2.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ディレクトリの下にパッケージごとにコンパイル結果が整理されます.