インターフェイスとしてのTrait

ScalaのtraitはScalaのオブジェクト指向機能で重要な役割を果たしています.このため,その機能をひとことで説明することは難しいのですが,ここではその部分的な機能としてオブジェクトの仕様を表現したインタフェイスと見做します.

インタフェイスについて学ぶ前に,インタフェイスを与えられる立場にあるオブジェクトについて観察してみましょう.たとえば,solution1.scalaの定義を眺めると以下のようにさまざまな定義があることがわかります.

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() ...
  }

このうち,このオブジェクトの利用者にとって興味があるのはどれでしょう.

このオブジェクトのそもそもの目的は,パズルの解を与えることがあります.このオブジェクトの利用者の立場からは,パズルの解を与えてくれるsolveメソッドがもっとも重要といえるでしょう.

一方,リスト中に出現する数を勘定するcメソッドのように細かい機能は,solveが期待される機能を果たしている限り,あまり興味はないことになります.多くのドライバーにとって,重要なものがハンドル,アクセル,ブレーキのような直接,ドライバーの操作の対象となる部位であることにも似ています.一方,重要性は明らかでも,操作の直接的な対象というわけではないエンジンルームの部品群への意識はなかなか向かないものです.

このような利用者の目線から眺めたSolution1オブジェクトの機能を抽出したものをオブジェクトの外部仕様といい,traitと呼ばれる形式で表現されます.

package puzzle
trait Solution {
val name: String
def solve(): List[List[Int]]
}

ここで,Solution1オブジェクトから取り出したtraitは,namesolveで,それぞれ解法名を表す文字列と解法にあたるメソッドです.解法を与えるsolveメソッドは引数はなしで,見つかった解の集合をリストとして与えることとなっています.

今度はsolution2.scalaで定義されるSolution2オブジェクトの内容を見てみましょう.

package puzzle
/**
* パズルの別解.コードの短さに驚愕するかもしれないが慌ててはいけない.
* Solution1.satisfyを借用している点が大きい.ただし,genCheck自体はイテレータを
* 用いることでかなり簡素化されていることは事実だ.
**/
object Solution2 extends Solution {
val name = "Solution 2"
def genCheck(): List[List[Int]] = {
val range = List.range(0, 9)
for (x1 <- range; x2 <- range; x3 <- range; x4 <- range
if Solution1.satisfy(List(x1, x2, x3, x4)))
yield List(x1, x2, x3, x4)
}
def solve(): List[List[Int]] = genCheck()
}

Solution2オブジェクトの要素としては,文字列namegenCheckメソッド,そしてsolveメソッドがあります.これらは前述のSolution traitを包含していることがわかりますね.逆に言えば,Solution2オブジェクトはSolution traitが要求する機能を提供しています.このため,Solution2オブジェクトがSolution traitを満足すると見做せます.このことを型として表すと,以下のように表現することができます.

Solution1: Solution

Solution2: Solution

Solution1オブジェクトもSolution2オブジェクトも異なる内容を持っていますので,厳密には型が一致するはずはありません.しかし,その部分要素に限れば,同じ型と見做しても構わないだろうというのがtraitを用いて表現されるインタフェイスの考え方です.

これらのオブジェクトをtraitを用いたインタフェイスとして抽象化することによって,これらの同質のものとして扱う技法はMain.scalaのなかで活用されていました.

1 List(Solution1, Solution2).foreach((solution: Solution) => {
2       println(solution.name)
3       solution.solve().foreach(println)
4     })

上のコードの1行目のList(Solution1, Solution2)において,Solution1とSolution2はともにSolution型と見做されて,List[Solution]型のリストの要素となります.つぎにList.foreachによってこのリストは走査されるため,このコード中のsolutionは,Solution1オブジェクト,あるいはSolution2オブジェクトを指すこととなります.

このような技法を用いない場合,Mainオブジェクトのコードには,以下のようにSolution1を処理する部分に続いてSolution2を処理する部分を別途記述せざるを得ません.人によっては,こちらの方が見易いと思うかもしれませんが,今後,解法が増える可能性を考えると冗長性の問題を残しているといえます.

1 println(Solution1.name)
2       Solution1.solve().foreach(println)
3 
4       println(Solution2.name)
5       Solution2.solve().foreach(println)

ソフトウェアのなかに類似する機能が含まれる場合に,その類似性をtraitで表現したインタフェイスとして抽象化することによって,プログラム内の冗長性を削減する技法を覚えておきましょう.