Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
17
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

クラス線形化のまじめな解説

この記事は Scala Advent Calendar 2015 (Qiita版) の19日目の記事です。

「右から左」程度のあいまいな説明で済まされることの多い Scala のクラス線形化(線形化順序)について,言語仕様できちんと定義を確認して,心の中のもやもやを解消したいと思います。

線形化とは

下図のように,あるクラスに複数のトレイトがミックスインされていて,同名のメソッドの異なる実装が複数のクラスやトレイトに存在する場合,どのメソッドの実装を呼び出すべきかという問題が発生します。この問題を解決するための一つの方法は,トレイトとクラスをある規則にしたがって一列に並べて,その並びにしたがって呼び出すメソッドを決めるという方法です。これがクラスの線形化です。

以下のプログラムは,上図と同様のクラスとトレイトを定義したプログラムです。CustomPrinter クラスは Printer を継承していて,PrintsTwicePrintsWithFaces がこの順でミックスインされています。


class Printer {
  // 文字列をそのまま印字する
  def print(str: String) = println(str)
}


trait PrintsTwice extends Printer {
  // 文字列を2回分印字する
  override def print(str: String) =
    super.print(str * 2)
}

trait PrintsWithFaces extends Printer {
  // 文字列に顔文字を加えて印字する
  override def print(str: String) =
    super.print(str + "(^^)/")
}

class CustomPrinter
    extends Printer with PrintsTwice with PrintsWithFaces

val ptr = new CustomPrinter
val () = ptr.print("hello") 

さて,このプログラムをコンパイルして走らせたときの挙動は以下のどれでしょうか?

  • hello と表示される(Printer の実装が呼び出される)
  • hellohello と表示される(PrintsTwicePrinter の実装がこの順で呼び出される)
  • hello(^^)/ と表示される(PrintsWithFacesPrinter の実装がこの順で呼び出される)
  • hellohello(^^)/ と表示される(PrintsTwicePrintsWithFacesPrinter の実装がこの順で呼び出される)
  • hello(^^)/hello(^^)/ と表示される(PrintsWithFacesPrintsTwicePrinter の実装がこの順で呼び出される)
  • そもそもこのプログラムはコンパイルを通らない

正解は5番目の「hello(^^)/hello(^^)/ と表示される」です。確かに,「右から左」に,PrintsWithFacesPrintsTwicePrinter の実装がこの順で呼び出されていることがわかります。この例では,CustomPrinter とその祖先は以下のように線形化されます。

CustomPrinter -> PrintsWithFaces -> PrintsTwice -> Printer (-> AnyRef -> Any)

では,次の例の線形化はどうなるでしょうか? (答えは後半で)

なぜ「右から左」か

Scalaにおけるクラス線形化の定義を見る前に,なぜ「右から左」にクラスを探索していくのか考えてみましょう。

今回の例の CustomPrinter は以下のように定義されています。

class CustomPrinter
    extends Printer with PrintsTwice with PrintsWithFaces

一般に,キーワード with の直後に書かれるトレイトは,その with の左側で定義されるクラスへミックスインされます。この例では,CustomPrinter は「「「Printer を継承したクラス」に PrintsTwice をミックスインしてできたクラス」に PrintsWithFaces をミックスインしたクラス」であると定義されています。心の眼で括弧を補うと,このコードは次のように見えることでしょう。

((class CustomPrinter 
    extends Printer) with PrintsTwice) with PrintsWithFaces

Scalaではこのようにミックスインの順序が定められているので,with の直後に書かれるトレイトは,その with の左側で定義されるクラスの外側に実装を持つべきです。今回の例で言えば,右端のトレイト PrintsWithFaces のメソッド print は一番外側で実装されるべきであることがわかります。

そして,メソッドの実装の探索は「外から内」すなわち「右から左」の順番に行われます.これが,線形化の順序が「右から左」である所以です。下の図が理解の助けになればいいのですが...。

線形化の定義

では実際にScalaにおけるクラス線形化の定義を仕様書で確認してみましょう。Scala Language Specification Version 2.11 によると,クラス線形化の定義は以下のようにあります(Section 5.1.2,訳は筆者による)。

定義: 線形化
$C$ をテンプレート C_1 with ... with C_n { stats } を持つクラスとする。$C$ の線形化 $\mathcal{L}(C)$ を以下のように定義する。

\mathcal{L}(C) = C, \mathcal{L}(C_n) \mathop{\vec{+}} \cdots \mathop{\vec{+}} \mathcal{L}(C_1) 

ここで,$\vec{+}$ は連結を表す。ただし,この連結において,右オペランドの要素は左オペランドの同一の要素に取って代わる。

\begin{array}{r@{\;}ll}
a, A \mathop{\vec{+}} B & = a, (A \mathop{\vec{+}} B) & \text{if $a \notin B$} \\
 & = A \mathop{\vec{+}} B & \text{if $a \in B$} \\
\end{array}

前半はいわゆる「右から左」のルールを形式的に述べたものです。後半は親より子が先行するというルールに相当します。

実際の例に当てはめてみましょう。今回の例では $\mathcal{L}($CustomPrinter$)$ は以下のように計算されます(ここでは簡単のため AnyRefAny の存在は無視しています)。

\begin{align*}
\mathcal{L}(\mathtt{CustomPrinter}) & = \mathtt{CustomPrinter}, \mathcal{L}(\mathtt{PrintsWithFaces}) \mathop{\vec{+}} \mathcal{L}(\mathtt{PrintsTwice}) \mathop{\vec{+}} \mathcal{L}(\mathtt{Printer})\\
 & = \mathtt{CustomPrinter}, \mathtt{PrintsWithFaces}, \mathcal{L}(\mathtt{Printer}) \mathop{\vec{+}} \mathtt{PrintsTwice}, \mathcal{L}(\mathtt{Printer}) \mathop{\vec{+}} \mathtt{Printer} \\
 & = \mathtt{CustomPrinter}, \mathtt{PrintsWithFaces}, \mathtt{Printer} \mathop{\vec{+}} \mathtt{PrintsTwice}, \mathtt{Printer} \mathop{\vec{+}} \mathtt{Printer} \\ 
 & = \mathtt{CustomPrinter}, \mathtt{PrintsWithFaces}, \mathtt{PrintsTwice}, \mathtt{Printer} 
\end{align*}

すなわち,CustomPrinter とその祖先の線形化は以下のようになります。

CustomPrinter -> PrintsWithFaces -> PrintsTwice -> Printer (-> AnyRef -> Any)

さて,「答えは後半で」とだけ言い残した例の線形化ですが,定義に忠実に計算していけば答えを導くことができます。結果は以下の図のようになります(右肩に線形化後の順序の番号を振ってあります)。

余談ですが,total な関係(どの2つの要素を持ってきてもそれらが比較可能であるような関係)のことを linear な関係と呼んでいる文献を見たことがあります。今回の線形化もある意味で total な関係の構築であるわけで,linear な関係という表現との整合性がうまくとれているなとちょっと思ったりしました。

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
17
Help us understand the problem. What are the problem?