読者です 読者をやめる 読者になる 読者になる

翻訳: "Cake Pattern: The Bakery from the Black Lagoon"

はじめに

NEScala 2013 での Daniel Spiewak (@djspiewak) さんの基調講演 "Cake Pattern: The Bakery from the Black Lagoon"スライド動画*1の抄訳です。本記事の公開については、講演者の Spiewak さん、および配信元である Marakana Inc. さんに許可を頂いています。Thank you for your great kindness!

講演者の Spiewak さんは、Scala のディープな活用で有名な Precog の開発に携わっている方です。”モナドはメタファーではない (Monads Are Not Metaphors)”の著者でもあります。

この講演は、昨年のブログ記事 "Existential Types FTW" の議論をさらに掘り下げたもので、Precog での "Cake Pattern" の実践について語った大変興味深い内容となっています。(ちなみに "Existential〜" の続きが、@eed3si9n_ja さんが翻訳された”抽象的な Future”

例によってリスニングや意味の把握が怪しい箇所がたくさんあるので、随時ご指摘を頂ければ幸いです…。

【07/17 追記】eed3si9n さんから頂いたご指摘を反映しました。多謝。

【10/19 追記】講演中で言及されている Precog のコードベースがオープンソース化されました。Cake Pattern の実例も出てくるようなので要チェック。

【12/16 追記】本講演と同様の内容を解説しているスライド

また、先頃開催された Scala Conference in Japan 2013 で、望月さんが同様のテーマで講演(スライド動画)をされているので、そちらも併せてご覧になると良いかと思います。

Introduction

  • "Cake Pattern" という名前は、Jon Pretty が 7 年前に命名した。Cake Pattern がメジャーになったのは、Jonas Bonér が 4 年前に書いたブログ記事(翻訳)で、Cake Pattern による依存性注入の手法を紹介してから。
  • しかし、 依存性注入は Cake Pattern のほんの一側面でしかない。

サンプルコード

  • Cake Pattern のサンプルコード。
    • UserModuleTweetModuleがある。UesrModuleloadUser()でユーザを読み出す。MySQLUserModuleUserModule を実装している。
    • TwitterModuleは、TweetModuleUserModuleの両方を使う(MySQLUserModuleではなく、より抽象的なUserModuleを使う)。
trait UserModule {
  def loadUser(id: Long): User
}

trait TweetModule {
  def post(userId: Long, body: String)
}

trait MySQLUserModule extends UserModule { … }

trait TwitterModule extends TweetModule with UserModule { … }

val universe = new MySQLUserModule with TwitterModule {}
  • 最後に、”宇宙の終わり(at the end of the universe)”で全てを組み立てる。
    • universeは、MySQLUserModuleTwitterModuleを組み合わせる。コンパイラが、列挙された依存関係にloadUserpostが実装されているか確認してくれる。

型理論 (Type Theory)

状態の隠蔽

  • private フィールドのxと、xと引数yを使って新たな値を返す関数addを持つクラスFooを考える。
class Foo(x: Int) {
  def add(y: Int) = x + y
}
  • ここでaddは、”Fooオブジェクト内に隠蔽された状態x”を暗黙的な第一引数として受け取る関数、とみなすことができる。

型の隠蔽

  • 以下のコードで、apply関数は型パラメータAをとる。これは、全てのAで正当化される。applyAを与えるとOption[A]が得られる。
trait Forall {
  def apply[A](a: A): Option[A]
}
  • ここでは型シグネチャに注目する。これは \textstyle \forall (for all) だ。論理学を知ってる人なら、全称量化 (universal quantification) と存在量化 (existential quantification) についても知っていると思う。
    • 全称量化(\textstyle \forall_{a} {T[a]})は、 全ての \textstyle a について \textstyle T[a] が真であると言っている。
    • 存在量化(\textstyle \exists_{a} {T[a]})は、 ある \textstyle a について \textstyle T[a] が真であると言っている。僕らは \textstyle a が何であるか知っている。
  • つぎに \textstyle \exists (for some) を Scala で表すと以下のようになる。
trait Exists {
  type A
  def apply(a: A): Option[A]
}
  • Existsトレイトでは、AapplyのパラメータではなくExistsの内側にある型になっている。
  • このとき、以下のようにExistsapplyメソッドを呼んでもうまくいかない。
def bippy(e: Exists) = e(42)
  • このコードがエラーになる理由:
    • FooAは”全ての A は”なので、任意のAインスタンスを与えることができる。
    • ExistsAは”ある A は”なので、僕らはAが何であるかを知らない。だから、コンパイラAIntであると証明できない。
    • つまり、Aに妥当な型を置くことができないので、applyメソッドを呼ぶことができない。
  • これを型シグネチャで表すと \textstyle T_1 \rightarrow T_2 ではなく \textstyle \exists_{X} X \rightarrow T_2 となる。
    • つまり、ある \textstyle X が存在するとき、\textstyle T_2 がいつでも存在する。
    • この型シグネチャはいくつかの情報を捨てているが、同時に \textstyle \exists_{X} を使って何かすることを示している。
  • なぜ、開発者に対してクラスが持つ型を隠すことが有用なのか?
    • 情報の隠蔽。 それが、モジュール化の本質であり、オブジェクト指向の中核的な原則だから。
    • コードベースの複雑さを管理するには、情報を分割して、コードベースのある部分が他の部分の詳細を知らないようにし、独立に改良できるようにする必要がある。

存在型

(訳注: Scala で存在型といえばforSomeのことだが、ここでは上述の存在量化を指している。また、\textstyle \forall_{a} {T[a]} に対応する普通のジェネリクスは普遍型 (universal type) と呼ぶ。)

  • Thingトレイトは、その内部に存在型 (existential type) であるXを持つ(ここでは型パラメータA, Bは重要ではない)。
trait Thing[-A, +B] {
  type X

  def source(a: A): X
  def sink(a: X): B

  def apply(x: X): X
}
  • ここで存在型Xは、オブジェクトの内部状態の表現として考えることができる。
    • Fooが内部状態として private フィールドxを持つように、型Xは状態であると考えることができる。XIntかもしれないし、他の何かかもしれないが、それをクラスの外側から知ることはできない。
    • コンストラクタのように渡すことで、状態を作ることができる。また、その状態を使ってsinkメソッドから値を生成できる。
  • Scala の魔法を使うと、Xが何の型なのか知らなくてもThingを使うことができる。
def bippy(t: Thing[Int, String]) = {
  val state: t.X = t source 42
  val state2: t.X = t(state)
  t sink state
}
  • sourceメソッドに入力型のIntを与えるとXを取得できる。そして、Xを使ったり渡したりして最終的な結果を生成できる。
    • これは、private メンバを持つオブジェクトを使う時に起こる事とよく似ている。違いは、メンバの場合は JVM が暗黙的に解決してくれるのに対し、代わりにプログラマが明示的にやっていることだ。
  • 情報隠蔽と存在型を使ったモジュール化のコンセプトは、それについてあまり深く考えたことがないだけで、僕らが毎日のようにやっていることだ。
  • Cake Pattern を使うと、存在型を使った情報隠蔽のコンセプトを活用できる上に、もっと強力なモジュールを手に入れることができる。

モジュール性

  • Cake Pattern の本質はモジュールで、さらにトレイトはモジュールだ
  • この講演の残りの時間では、パッケージを使わないことにしたい。
    • パッケージはひどいし、使うべきではない。パッケージは、名前につくファンシーな接頭辞でしかないし、プログラミングの道具としてあまり良いものじゃない。
    • トレイトこそ真のモジュールだ。組み合わせられるし、管理できる。
  • パッケージと違い、トレイトは型検査される依存関係 (type-checked dependency) を明示的に記述することができる。
    • パッケージも import に失敗したらエラーが出るので、型検査される依存関係を提供しているように見える。だけど、パッケージの外側を見ることができないし、import したものが全て見えてしまう。
    • また、パッケージ同士を組み合わせることができないし、特定の方法で import した元のパッケージの中で import されたものを変更することもできない。
  • 同様に、トレイトは完全なカプセル化を提供する。
    • トレイトの中にはメソッドの実装だけでなく、型の実装も隠すことができる。
    • API の消費者に対して詳細を見せる必要はない。消費者は、”特定のThingに対する正しい型Xがあり、このXは関数bippyに渡されて連携する”といった詳細を知る必要がないからだ。消費者は、型の実装が何であるかを気にかけたりしない。
    • そうした詳細を隠すことで、とても素晴らしいカプセル化が可能になる。そして、伝統的なパッケージが提供してきたものも手に入る。
  • Scala のような言語では、この方法でトレイトを使うことでオブジェクト指向プログラミングができる。
    • 僕らは関数型プログラミングから得られる利益を知っているし、僕も Haskell 的な高階関数スタイルのプログラミングや抽象化を使うことが多い。それはとても素晴らしいし重要なんだけど、とても巨大な論理的モジュールを構造化する時には、僕は Cake Pattern を使ったオブジェクト指向へと立ち戻る。
  • Cake Pattern は実務でとても役に立つし、今や、大規模なコードベースで実践済みだ。問題もあるが、全体としては驚異的に有益な経験だ。
  • では、君のコードベースに Cake Pattern を配備するプロセスを、三つのステージで考えてみよう。

Stage 1: 導入

  • ステージ 1 は Cake Pattern の導入。
  • まず、君のコードベースをトップレベルのトレイトに分割する。
    • 基本的には、一つのトレイトごとに一つのファイルを用意するといい。各ファイルはトレイトに関する1以上のレベルを意図しており、それで全てだ。
  • これだけでも、多くの利益が得られる。
    • ファイル A の中にあるものにアクセスするには、そのトレイトをファイル B の中にミックスインする必要がある。
    • 今や君は、明示的にドキュメント化され、型チェックされた A - B 間の依存関係を手にしている。以前は、依存関係が暗黙的だったのと対称的だ。
  • モジュールとしてのトレイトは importの代わりに、継承階層であるextendswithを使う。
  • 普通のファイルと違い、トレイト内では”裸の関数”を使うことができる(本当はトレイトのメンバだが、”Cake の中”では裸の関数として扱える)。
    • 裸の関数を使うのは、時にとても素晴らしい。なぜなら、JVM の奇妙な癖を回避するため、関数をラップするためだけに無意味に関数をクラスの中に置くことがあるから。
  • 全てをトレイトの基底スコープに置く必要はない。名前衝突が起きた時は、内部オブジェクトを使って名前空間を分ける、

例:

  • UserModuleの中には二つの関数loginsaveとクラスUserがある。
trait UserModule {
  def login(name: String, pass: String) = {
    // do stuff
  }

  def save(user: User) {
    // do more stuff
  }

  case class User(...)
}
  • ここでは単にトレイトの中に入れただけだ。Cake Pattern を使わずに、単にこれらをファイルの中に置くやり方も容易に想像できるだろう(UserModuleの中は全て具体的な実装で、本当はトレイトだけど、ここでは気にしない)。
  • MessageModuleはメッセージの作者を扱うためにUserModuleインポート (import) する。
trait MessageModule extends UserModule {
  def render(msg: Message): Group[Node] = {
    // yay, anti-xml user!
  }

  def save(msg: Message) {
    // stuff
  }

  case class Message(author: User, body: String)
}
  • 全ての依存関係は型シグネチャにあるので、暗黙にインポートする必要はない。”世界の終わりで”一緒になる時には、それらの両方を持っていることが保証される。

名前空間としての内部オブジェクト

  • save関数はUserModuleにもMessageModuleにもある(ので名前が衝突してしまう)。ここではsave関数が受け取る型が違うので、コンパイラはこれを区別できる。しかし、それはオーバーロードだ。オーバーロードはキモいし酷いし C++ 時代の遺物なので、できれば回避したい。
  • これを回避するため、save関数はmessageオブジェクトの中に押し込んでしまう(メッセージ以外と関連しない関数だから)。
trait MessageModule extends UserModule {
  object message {
    def render(msg: Message): Group[Node] = {
      // yay, anti-xml user!
    }

    def save(msg: Message) {
      // stuff
    }
  }

  case class Message(author: User, body: String)
}
  • これは名前空間だ。内部オブジェクトを使った名前空間は、モジュール化のためではなく、曖昧さをなくして名前衝突を避けるためのものだ。
  • パッケージと違って、オブジェクトには実体がある。名前空間としてのオブジェクトは、他の Scala オブジェクトと同様に、束縛したり、リネームしたり、持ち歩いたり、渡したりできる。

Stage 2: 関数と型の抽象化、ライフサイクル管理

  • 次に問題になるのはテストだ。単体テストは良いことだが、実践はとても難しい。高レベルのモジュールが他のモジュールを使うとき、それらのいくつかはリモートサービスと対話したりする。
  • そうしたモジュールをテストするのは複雑になるので、代わりに高レベルモジュールをテストしたい。そこで、僕らはモックを使う。Spring 等の依存性注入フレームワークを使った大規模システムでモックを使ったことがある人は、それがいかに大変かを知っているだろう。
  • Cake Pattern は、これをやるとても良い方法だ。
    • 僕らのモジュールはトレイトであることを思い出そう。トレイト内の関数は抽象化して、実装はどこか別のところに置く。そしてモジュールは、他の(具体的な実装ではなく)抽象的なトレイトに依存する。
    • 抽象的なトレイトに対して、本番システム用、開発用、テストスイートなど、具体的な実装を複数作ることができる。これによって高い柔軟性が得られ、例えば、ストレージ・バックエンドをすぐさま取り替えるといったことができる。
  • 関数をリファクタリングして抽象化しよう。新しいモジュールを作るときは具体的な関数を使うが、すぐさま抽象的な形に移行する。
  • 関数だけでなく、型も抽象化する。
    • 具体的な型に甘んじる必要はない。これは Scala における仮想クラスで、パッケージだけではできないことだ。
  • 型境界によって抽象型を洗練し、複数のモジュール間で定義のための仮想型を持つ。
  • Cake Pattern にとってライフサイクル管理は永遠の課題だ。
    • 純粋でない (impure) 宇宙では、モジュールには起動ルーチンと停止ルーチンが必要なので、通常は恐ろしい副作用がある。
    • 副作用である初期化ロジックをコンストラクタに書くと酷い目に遭う。代わりに Stackable Traits Pattern を使うべき。Stackable Traits Pattern は、Scala の中で最も誤解されている修飾子だ。

関数を抽象化する

  • UserModuleに戻って、loginメソッドとsaveメソッドを抽象化してみよう。
trait UserModule {
  def login(name: String, pass: String): Option[User]

  def save(user: User): Unit

  case class User(...)
}
  • これでUserModuleは動作の詳細を持たなくなった(loginがどのように認証するか、とか)。関数は抽象的な型付きシグネチャなのでUserModuleの関数を使って何かすることはできるが、その詳細を知ることはない。
  • 次に、MongoDB を使ってUserModuleの認証等を実装する。
trait MongoUserModule extends UserModule {
  def login(user: String, pass: String) = {
    ...
  }
  ...
}

trait MessageModule extends UserModule {
  ...
}
  • そして、MessageModuleは抽象化されたUserModuleを取り込む。ここで依存するのはMongoUserModuleではないUserModuleだけで必要な情報を十分に提供しているからだ。
  • 情報隠蔽がモジュール化の本質である、と言ったのを思い出してほしい。MessageModuleが関心があるのはloginsaveだけなのだから、MongoDB の詳細は隠蔽したい。

型を抽象化する

  • ところでUserオブジェクトにsave機能を持たせたい場合はどうするか?
  • オブジェクト指向言語では、saveを引数にUserを取る関数とするのではなくUser上の関数とすることができる。どちらも副作用があるのでUnitを返す。
trait UserModule {
  ...
  case class User(...) {
    def save(): Unit = // uh?
  }
}
  • これはUserModuleの抽象化を壊している。このモジュールを完全に抽象化するには、Userを保存する方法の詳細を取り除くべきだ。protected な抽象関数が実際には保存処理を行っている、というようなことがあると、バージョン管理で間違いなく酷いことが起きる。
  • 僕らにできるのはUserを抽象化することだ。
trait UserModule {
  ...
  type User <: UserLike

  trait UserLike { this: User =>
    def id: String
    def name: String

    def save(): Unit
  }

  def User(id: String, name: String): User
}
  • ここではUserを抽象化してUserLikeを継承するように制約する。UserLikeUserシグネチャを定義する抽象クラスだ。UserLikeにはidname、そしてsave関数がある。
  • さらに、UserLikeUserへ制約する自分型 (self-type) を持つ。この場合、何も関数が実装されてないので意味はないが、自分自身 (this) を返す関数を定義する時に必要になる。
  • 一番下にあるのは、Userの抽象コンストラクタだ。idnameを取ってUser型の値を返す。UserModuleを取り込んでUserを使う際に、メソッドの実装だけでなく、抽象型であるUserを実際に実装する型が何であるか知らなくても良い。

仮想クラスの実装

  • ここまでに、メソッドレベルの抽象化と、型レベルの抽象化の両方を見てきた。これはとても強力だ。
  • MongoUserModuleではUserLikeを継承したUserクラスを定義し、コンストラクタを定義する。仮想クラスを実装して型階層の中にまとめるにはこれで十分だ。
trait MongoUserModule extends UserModule {
  ...
  class User(...) extends UserLike {
    def save(): Unit = {
      ...
    }
  }

  def User(id: String, name: String) = new User(id, name)
}
  • もしMongoUserModuleを持たないなら、使えるのはUserModuleだけなので、このときUserは抽象化された形で使う。
  • 興味深いのは、最初に話した”存在型”のコンセプトへとどんどん近づいていることだ。情報隠蔽はクロージャとかの中に押し込むだけでなく、型制約をつかって型レベルで隠蔽することもできる。

ライフサイクル管理

  • ここにLifecycleモジュールがある。起動 (startup) ルーチンや停止 (shutdown) ルーチンが必要なモジュールは、コンストラクタやファイナライザに書かずに、僕らが制御可能なstartup関数とshutdown関数に実装する。
trait Lifecycle {
  def startup(): Unit
  def shutdown(): Unit
}
  • 問題は、起動・停止ルーチンがあるモジュールが複数ある場合だ。ルーチン内から各モジュールが自分以外のモジュールへと次々と委譲していくのは、非常に複雑になるのでやりたくない。本当にやりたいのは、startupルーチンの一部として何かをすると同時に、自分がまさにしたいこと以外には関心を払わずともstartupルーチンの他の部分を開始することだ。
  • これがabstract override修飾子の目的だ。かつて僕が Scala の仕様書を読んでabstractoverrideを一緒に使えると分かったとき、これは typo だと思った。意味が分からなかったんだ。
trait MongoUserModule extends UserModule with Lifecycle {
  abstract override def startup() {
    super.startup()
    initMongoThingy()
  }

  abstract override def shutdown() {
    killMongoThing()
    super.shutdown()
  }
  ...
}
  • abstract override は、基本的にはトレイト内のsuperポインタへアクセスできるようにする。これは本当のポインタではなくて、名前空間のようなものだ。
  • これが Artima の Stackable Trait Pattern だ。MongoUserModuleの中に隠蔽されたinitMongoThingy関数は、起動サイクルの一部として呼び出される。このモジュールの起動サイクルは、既知宇宙にある他のモジュールの起動サイクルへと再委譲する。同じ事がshutdownについても言える。
  • これにより、モジュール同士を組み立てる際に、全てのモジュール自身のライフサイクルを一緒に起動したり停止したりできる。
  • 少し注意が必要なのは、これらのライフサイクル関数の正確な実行順序は非決定的ではないが、非決定的だと思って扱うべきだということだ。実行順序は Cake をどのように組み立てたか、つまりwith節でMongoUserModuleMessageModuleの前に来るか後に来るかの順序に依存する。
  • ライフサイクルの起動ルーチン(あるいは停止ルーチン)間に依存関係を持つべきでないということは、重要なので覚えておいてほしい。あるモジュールが、それより先に開始する他のモジュールを必要とするなら、ファクトリをスタックに積む等の方法を使う必要がある。両者を単純にスタックに積んで、それが動作するように祈ることはできない。
  • しかし一般的には、ほとんどのモジュールは自己完結しているので、こういったライフサイクルをスタックに積むパターンはとてもうまくいく。

Stage 3: 合成とネスト化

  • 継承(withextends)によるインポートはとても良いものだし、予期しない制限もないので使うのをやめる必要はない。
  • しかし、継承より合成 (composition) を使うべき場合はある。モジュールが”状態を持った何か”に依存していて、その状態を Cake の外側や、またはそのモジュールの他のインスタンスと共有する必要がある場合は、withextendsによる継承を使うことができない。代わりにモジュール内でvalを使う。
  • もう一つは、モジュール内にモジュールがネストしている場合だ。
    • パッケージの中にあるパッケージは、単に名前に接頭辞を追加するだけだ。
    • トレイトは真のモジュールだ。これらは互いにネストできるが、これは単なる接頭辞の付与ではなくポリモーフィズムだ。
  • この方法で、独立したライフサイクルを持つことができる。
    • 外側のトレイト(=モジュール)が自身のstartupshutdownを持つとき、外側のモジュールの生存期間中に、その内側のトレイト(=サブモジュール)の生成と破棄を何度も繰り返したいことがある。
    • それをやるにはモジュールの中にモジュールをネストさせる必要があるが、これは少しばかり難解だ。

valによる合成

  • では、これがどのように見えるか見ていこう。ここにSystemModuleがあり、その中にUserModuleがある。
trait SystemModule {
  val userModule: UserModule

  def doStuff() = {
    userModule.makeUsers()
    userModule.encourageMemes()
    // ???
    userModule.profit()
  }
}
  • valを使うのは、UserModuleのライフサイクルがSystemModuleのそれとは独立しているからだ。SystemModuleUserModuleを継承させるよりも、このように合成を使った方がいい。そしてval userModuleSystemModuleの定義は、どこかで互いを Cake にした時に行われる。
  • 同様に、UserModuleをパラメータに取る一般的な関数適用とすることもできる。ここではSystemModuleは実際の参照を持たない。
trait SystemModule {
  def doStuff(userModule: UserModule) = {
    userModule.makeUsers()
    userModule.encourageMemes()
    // ???
    userModule.profit()
  }
}

モジュールのネスト化

  • 続けてStorageModuleを見てみよう。StorageModuleSystemModuleの一部だ。
trait StorageModule {
  def store(id: Long, data: ByteBuffer)
  def retrieve(id: Long): ByteBuffer
}

trait SystemModule extends StorageModule {
  def doStuff(userModule: UserModule) = {
    ...
  }
}
  • StorageModulestoreretrieveという抽象関数を定義する。ここでは具体的な実装は定義しない。
  • 問題は、UserModuleStorageModuleへアクセスする必要があるということだ。UserModuleには、ユーザをsaveしたりloginしたりする能力があることを思い出してほしい。もしSystemModuleのライフサイクルをUserModuleから独立させたくても、素朴に全てを継承で組み合わせると、互いのライフサイクルが結びつけられて二度と分けることができない。
  • では、UserModuleリファクタリングしよう。
// curse you, naming conventions!
trait UserModuleModule extends StorageModule {
  trait UserModule {
    type User <: UserLike
    ...
  }
}

trait SystemModule extends StorageModule with UserModuleModule {
  def doStuff(userModule: UserModule) = {
    ...
  }
}
  • UserModuleUserModuleModuleになった。酷い名前だが、UserModuleModuleUserModuleを含んでいる。面白いのは、UserModuleModuleStorageModuleから派生していることだ。Cake の中にStorageModuleを入れると、UserModuleと共に動作できる。
  • UserModule自身は独立している。起動したり、停止したり、任意個のモジュールを作ることができる。普通のオブジェクトなので、単に渡して回ったりもできる。
  • トレイトはモジュールを一級市民 (first class) にする。なぜならトレイトは、モジュールを管理したり、生成したり、破棄できるようにし、またオブジェクトの階層内で、隣り合う並列なモジュールを持つことができるからだ。これこそが Cake Pattern のとても価値ある性質で、これまでのパッケージやモジュールでは完全に手の届かないものだ。

落とし穴とベストプラクティス

  • 残念ながら、Cake Pattern を実践しようとするといくつか問題が生じる。そのほとんどは、僕らが JVM に立脚していることに起因する。

初期化順序

  • 起きうる問題の古典的な例を挙げよう。このプログラムは何を出力するだろうか?(NullPointerExceptionではない)
trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World"
}

class C extends B {
  val foo = "Hello"

  println(bar)
}
  • 実際に出力されるのはnullWorldだ。barの値は"Hello World"ではなく"nullWorld"になる。これはまったく直感に反している。ここにあるのはどれもvalであり再代入されない。値が欠けているのに気づくのは理論的に無理だ。
  • 問題は、僕らが JVM に立脚しており、またvalが正格 (eager) な構文であることだ。そのため、Scala は実行パス中でトレイトのコンストラクタ内のval構文へ到達すると、それを即時に実行する。他のあらゆるものの状態や、そのvalが何を使うか否かや、そのvalが使うものが利用可能であるか否かにお構いなしに。
  • 同様の問題は Java でも見られるもので、StackOverflow へ行くと一年中誰かがこれに関するスレッドを立てている。なぜなら、人々にとってこれに非常にややこしいからだ。
  • クラスCの初期化の過程を見ていこう。まず、Cコンストラクタが実行される。Cコンストラクタが真っ先に行うのは、Aコンストラクタへと委譲することだ。Aコンストラクタには何もないので、Bコンストラクタへ委譲する。
  • 問題はBコンストラクタbarだ。barfooにアクセスして、それを"World"という文字列と連結する。しかしfooはまだ生成されていない。なぜなら、Cコンストラクタは自身の親コンストラクタへ委譲している最中で、まだ実行されていないからだ。BコンストラクタCコンストラクタへ委譲を返し、ここで問題が起きる。
  • これはトレイトを使うと非常によく起きる問題で、そして Cake Pattern は全てがトレイトなので、この問題を頻繁に目にすることになる。一般的に、副作用の有無に関わらず、正格評価 (eager evaluation) は副作用のもう一つの形態だ。これは初期化順序の問題で、ほとんどの場合、コード中の起こるはずがない部分でのNullPointerExceptionとして現れる。理不尽なことが起きたら初期化順序を疑うのは、一般的に良い習慣だ。

予期しない JVM のゼロ

  • JVM における予期しない”ゼロ”は、いつでもnullであるとは限らない。falseかもしれないし、IntLongの場合は0かもしれない。これに対処しておかないと、問題の切り分けがとても難しくなるし、階層を線形化する方法を把握できなくなることがある。

解決策とパフォーマンス問題

  • Precog のコードベース内の Quirrel パーサには、興味深いバグがある。
    • 今、抽象化も継承もされていない、単なる private なvalがトレイト内にあるとする。もしそれがvalのままだと、コンストラクタ内で使われておらず、コンストラクタ階層へアクセスしていないにも関わらず、パーサは動作しない。クラッシュするわけではなく、単に誤ったものを渡してくる。
    • パーサ関数を構成するには、それをlazy valにする必要がある。僕にはこれがなぜそうなるのか分からないし、全く理解できない。
  • これに対する一般的な解決策は、valの代わりにdeflazy valを使うことだ。lazy valは、正格評価を遅延評価 (lazy evaluation) にすることで問題を回避する。
  • Scala コンパイラ自身にも興味深い問題がある。scalac のソースコードは、かなり以前から Cake Pattern を使用している。
    • scalac は、コンパイラが知る必要のあるポインタである多数のシンボルを持つ。それはクラスオブジェクトの名前であったり、Stringクラスの名前であったり、IntegerBooleanのボクシング (boxing) クラスであったりする。
    • コンパイラはそれらのシンボルを内部的に知る必要があるので、全てのシンボルを持つ巨大なトレイトが必要になる。問題は、scalac Cake の内部で定義されるシンボルの考え方だ。
    • セットアップされた Cake なしでシンボルを生成することはできない。scalac は、文字通り何百ものlazy valを持つトレイトを持つ。そして、scalac は特別な関数を使って自身でそれらのlazy valを正格評価するが、全てのlazy valを初期化するのに数十秒かかる。とても正気とは思えない!
  • lazy valはパフォーマンス問題だ。もし、とにかくパフォーマンスを気にするなら、lazy valは避けられる限り使ってはいけない。Cake Pattern を使う限りは、この問題を避けるのは難しい。可能ならdefを使い、もし必要ならlazy valを使う。
  • fooを抽象的なdefにし、barlazy valに変更すると問題は解決する。
trait A {
  def foo: String
}

trait B extends A {
  lazy val bar = foo + "World"
}

class C extends B {
  val foo = "Hello"

  println(bar)
}

初期化中のデッドロック

  • このコードのmoar.Foomoar.Barといったコンストラクタには初期化順序がある。
trait A {
  object stuff {
    object Foo
    object Bar
    object Baz
  }
}

trait B extends A {
  object moar {
    object Foo
    object Bar
    object Baz
  }
}
  • これらが相互に依存したり、一方が他方に依存したりしているとき、このコードを単一スレッドのプログラム中で初期化しようとするとデッドロックが発生することがある。これは狂気だ!
    • 問題はstuffオブジェクトとmoarオブジェクトだ。これらのオブジェクトはシングルトンなので、内部にコンストラクタを線形化するためのロックを持つ。ロックがないと、オブジェクトが並行的に生成されて複数のインスタンスができてしまう。
    • そして、オブジェクトがアクセスされる順序によっては、オブジェクトのロックが、その内部オブジェクトのロックとの間でデッドロックを起こすことがある。これは、オブジェクトが多重にネスト(オブジェクトのオブジェクトのオブジェクト…とか)していて、特に一番上がlazy valである場合に起きやすい。
    • プログラムの初期化中にデッドロックが起きて、特にそれが非決定的なデッドロックなら、おそらくオブジェクトのロック順序の問題だ。
    • 解決策は、lazy valの代わりにvalを使うことだ。(会場笑*2

まとめ

  • vallazy valdefのどれを使うのかは、本当に注意する必要がある。一般的には、lazy valを使って初期化順序の問題を避けるのが正解だ。しかし、lazy valはロックの順序問題を新たに生み出してしまう。
  • 僕らが Cake Pattern を使うときは、まずdefで始めることが多い。つぎに、defを使うことで奇妙なことが起きたら、代わりにlazy valを使う。そして、特定のlazy valデッドロックが起きることに気づいたら、代わりにvalを使う。さらに、初期化パターンによってはvalnullになることがあるので、その場合は泣きながら慎重にlazy valへ戻す。
  • 先ほどのコードを変換するとこうなる。
trait A {
  object stuff {
    val Foo
    val Bar
    val Baz
  }
}

trait B extends A {
  object moar {
    val Foo
    val Bar
    val Baz
  }
}

ツールの問題

コンパイル時間

  • Cake Pattern を使う時のもう一つの課題はコンパイル時間だ。これは Scala では一般的な課題だけど、Cake Pattern ではさらに問題になる。Quirrel コンパイラでは、ある時点で、9 個のソースファイルのコンパイルに三分半かかっていた。
  • sbt のインクリメンタルコンパイラは偉大だが、sbt がソースコードをフィルタできないと、恐ろしいことに、宇宙 (universe) の再コンパイルが必要になることがある。

コンパイルのバグ

  • 驚くべきことに scalac にはバグがある。型システムで複雑なことをしたり存在型を使うと、たくさんのバグに直面するだろう。特に、プレゼンテーションコンパイラが酷い。プレゼンテーションコンパイラは Cake Pattern を正しく処理できないため、ENSIME を使うのは諦めたほうがいい。Cake Pattern によって型エラーが滅茶苦茶になる。
  • (プレゼンテーションではない)普通のコンパイラも機嫌が悪くなることがある。全く意味不明な型エラーを見たことが何度もあるが、仕方がないので我慢するしかない。
  • sbt にも別の問題がある。最近のバージョンではもう解決していると思うが、sbt 0.11 では、本来再コンパイルが必要ないはずのものをインクリメンタルに再コンパイルしてしまうことで、必要のないものが変更されたり、最悪古いクラスファイルが置き去りになってしまうことがある。これが Cake に含まれていると、壊れずに、間違った意味論を持ったプログラムができあがってしまう。君がプログラムを実行すると誤った答えが出てくるなんて最高だ。原因を解明すると、単にクラスファイルが古いだけだったことが分かる。本当に意味不明だ。

Scaladoc

  • Scaladoc も Cake Pattern をうまく処理できないので、API ドキュメントが好きな人は注意しよう。

(ここで時間切れ。残りのスライドは、こちらの p.59 以降を参照。)

質疑応答

  • パッケージを全く使っていないわけではない。名前選択のために使っている。
  • 自分型 (self type) よりもextendswithの方が強力で管理しやすい。
  • 初期化問題の解決策としてトレイトの代わりに抽象クラスを使う。
  • Scala 2.9 と 2.10 のバグについて(レポジトリから scalac の最新版を取ってきて再現するか確かめることもやってるらしい)。

*1:題名の元ネタは”大アマゾンの半魚人 (Creature from the Black Lagoon)”と思われる。ロアナプラはたぶん関係ない、はず。

*2:直前に”val の代わりに lazy val を使え”と言ったばかりである…。