Future/Promise はいつモナドになったのか

「非同期計算をモナドで合成し、依存関係に従ってパイプライン化する」というアイデアはいつ誰が提案したのか、というのを調べてみたけどよく分からなかった記録。網羅的な調べ方はしてないので、何か知ってる人がいたら教えてください。

明示的 vs. 暗黙的

id:xuwei さんに教えて頂いた Wikipedia の記事によると「まだ完了していない計算結果へのプロキシオブジェクト」というコンセプトが FuturePromise と名付けられたのは 1976〜1977 年頃らしい。

1976 年に出た Daniel P. Friedman と David Wise の論文や Peter Hibbard の論文で言及されていた Promise(あるいは Eventual)は明示的 (explicit) に使うものだった。つまり、Java の(Completable じゃない方の)Future のように、promise から値を取り出すのに get のようなメソッドを呼ぶ必要があるということ。

一方で、アクターモデルの研究者である Henry Baker と Carl Hewitt による論文(1977 年)が言及している Future は、そのまま普通の参照のように使える暗黙的 (implicit) のものとされていた。

Promise Pipelining

次に、Promise 同士をつなぎあわせてパイプライン化しましょうというアイデアの登場は、Promise の発明から約 10 年後、1988 年の Barbara Liskov と Liuba Shrira の論文を待つことになる。また、同様のアイデアは、一部のアレゲな紳士諸君に著名な Xanadu プロジェクトの一環として Mark S. Miller*1 や Dean Tribble らからも 1989 年に提出されている。

ただ、これらの Promise Pipelining に関する論文の主眼は、同一マシン上に配置した Promise 間のネットワーク通信を削減したりして性能を向上させることなので、「非同期計算の合成」という点に限れば、もっと以前からアイデアはあったのかもしれない。

また、当時、これらの論文のまともな形の実装が世に出ることは無かったらしい。Wikipedia では、Promise Pipelining を実装した処理系として、後に Miller らが 1997 年に発表した E 言語 や、同様に Tribble らが 1996 年に発表した Joule 言語が挙げられている。

ちなみに、E 言語での記法はこんな感じ(x <- a() は「x にメッセージ a() を送る」と読ます):

t3 := (x <- a()) <- c(y <- b())

これを展開するとこうなる:

t1 := x <- a();
t2 := y <- b();
t3 := t1 <- c(t2);

どことなく Reactive Programming っぽい…?

JavaScript の Deferred

「Deferred に A => Deferred[B] を受け取って Deferred[B] を返すメソッド then() を持たせましょう」みたいなのは JavaScript ではかなり以前からあり、CommonJS では、先行例として MochiKitDojo's Deferred挙げている。少なくとも 2006 年頃にはあった模様。Twisted の Deferred は「明示的」なスタイルなのでちょっと違うかな。

最近は、Promises/A+ 仕様として標準化されて Thenable と呼ばれている。

Future/Promise モナド

結論から言うと、少なくとも Scala の実装においては、「Future/Promise はモナドである」というようなことを書いてる論文等があったり、それを参照して実装したりした、というわけではなさそう。

まず、Akka において公開レポジトリで辿れる最古の Future 実装を見ると完全に「明示的」なスタイルで、mapflatMap も見当たらない。

sealed trait Future[T] {
  def await : Future[T]
  def awaitBlocking : Future[T]
  def isCompleted: Boolean
  def isExpired: Boolean
  def timeoutInNanos: Long
  def result: Option[T]
  def exception: Option[Throwable]
}

その後 map追加されたりしたけど、ちゃんとモナドであることを意識した実装になったのはこの辺(2011 年 2 月)。下記のディスカッションによると、提案者の当初のモチベーションとしては、Akka に Scalaz の(ような?)型クラスを導入したいということだったらしい。

ちなみに、Twitter-Util の Future は、2010 年 8 月の公開当初からモナディックになってる。Marius さんは、Twitter の非同期処理系の API について Concurrent ML を参照しているとか言ってたので、あるいはそっち方面からアイデアを得たのかなぁ…?

追記: Scalaz の昔のコードを調べたらもっと古い実装があった(2009 年 5 月)。Scala 界隈だとこれが最古? どちらにせよ、リファクタリングの一環で自然に入ってきた的なノリを感じる。

まとめ

Haskell 方面の歴史を知ってれば一発なのかもしれないけど、Haskell の Future/Promise モナドに相当する型クラスってどれなんですかね…。Continuation モナド

追記

この Par モナドに成功/失敗の文脈を組み合わせると近いのかな。

追記2

*1:Wikipedia によると、今は Google のひとで、ECMAScript の仕様策定をやってる TC39 のメンバーでもあるらしい。

Java/Scala で風景から歩行者を消してみる

一昨日くらいにホッテントリ入りしてた記事↓を見て、

Export["result.jpg",
 Image[Mean[Map[ImageData,
    Import["movie.mov", "ImageList"]]]]]

このくらいのコードで済むなら Java/Scala でもすぐに書けるかも? と思ってやってみた。

理想

ヤッター、こんなに簡単にできたよー^^

import opencv._                               // ← ん?
System.loadLibrary(Core.NATIVE_LIBRARY_NAME)  // ← んんん???
saveImage("result.jpg", loadVideo("movie.mov")(mean))

現実

Isolator requires Java bindings for OpenCV. 

$ curl -OL https://github.com/Itseez/opencv/archive/2.4.9.zip -o opencv-2.4.9.zip
$ unzip opencv-2.4.9.zip
$ cd opencv-2.4.9
$ mkdir build
$ cd build/
$ cmake -DBUILD_SHARED_LIBS=OFF ..
$ make -j8
import org.opencv.core.{Core, CvType, Mat, Scalar, Size}
import org.opencv.highgui.{Highgui, VideoCapture}

package object opencv {
  ...
  def mean(frames: Iterator[Mat]): Mat =
    if (frames.hasNext) {
      val head = convertTo(CvType.CV_64FC3)(frames.next)
      val (count, out) =
        frames.
          map(convertTo(CvType.CV_64FC3)).
          foldLeft((1, head)) { case ((cnt, sum), f) => (cnt + 1, add(sum, f)) }

      divide(out, new Scalar(count, count, count))
    } else {
      new Mat
    }

  def loadVideo[A](filename: String)(f: Iterator[Mat] => A): A = {
    val cap = new VideoCapture(filename)
    try {
      f(Iterator.continually(nextFrame(cap)).takeWhile(_ != None).map(_.get))
    } finally {
      cap.release()
    }
  }
  ...
  def saveImage(filename: String, m: Mat) = Highgui.imwrite(filename, m)
}

^^;;;

考察

https://raw.githubusercontent.com/okapies/isolator/master/examples/mean-opencv.jpg https://raw.githubusercontent.com/okapies/isolator/master/examples/mean-opencv.jpg

Mathematica、画像や動画の読み書きをネイティブサポートしているのはホントにいいなぁ、という感想。

なんか、動画を扱える Pure Java の成熟したソリューションって未だにあんまりないらしくて、したがって OpenCVJava から叩くのが一番堅実な選択肢ということになり、その道の先には jar をソースからビルドする楽しい作業が待ってたり、コーディング時もリソースの明示的な解放をサボると一瞬でメモリが爆裂したりと、色々めんどくさい…。

コード

今回書いたコードは GitHub に置いておきますので、世紀末ごっこして遊ぶなり改造するなりどうぞ。

あと、mean のアルゴリズムはもう少しマシな方法があるように思うので、OpenCV に詳しい方、どなたかご教示くださいませ…。

追記

@colspan一晩でやってくれました

非同期ストリーム処理の標準化を目指す "Reactive Streams" とは

TL でこんなのが流れてたので少し調べてみた。

Reactive Streams って?

JVM 上でのノンブロッキングなバックプレッシャーを持つ非同期ストリーム処理の標準の提案”(公式サイトより)。

ざっくり言うと、既にある JVM ベースの様々な非同期ストリーム処理フレームワーク実装の共通部分を括りだして API 化、SPI 化しようというもの。最終的には JSR での標準化を目指している。

ここで言う”非同期ストリーム処理”とは、(広義の)リアルタイム性が求められるデータ処理中心のアプリケーション、より具体的にはビデオストリーミングや数百万ユーザのトランザクション処理など。

下のインタビュー記事では、Reactive Streams を策定する理由として、API 策定による相互運用性の向上と共に、バックプレッシャー (back pressure) による流量制御の必要性が挙げられている。

以上から分かるように、この "API" はユーザが直接使うものではなく、各フレームワークが、この API を使ってエンドユーザ向けの DSL を提供するためのもの。これにより、異なるフレームワーク間の相互運用性を担保できる。逆に言うと、ユーザがデータ変換や分割・結合を記述する方法は守備範囲ではない

なお、この仕様が出てきた背景としては、去年の秋くらいから "Reactive Manifesto"日本語訳)というキャンペーンが始まっていて、Reactive Streams はその流れに連なっていると思われる。

どんな仕様なの?

概要がこの辺に、より詳細な仕様がこの辺に書かれている。

現状、SPI コンポーネントの構成要素は以下の三つ。普通の Pub/Sub だけど、バックプレッシャーを伝える方法が定義されているのが特徴?

  • Publisher
    • Subscriber からの要求に応じて、潜在的に無制個の順序付けされた要素を提供する。Publisher は複数の Subscriber に配信することができ、処理率に応じて配分する。
    • これ以上、要素を提供できないときは Subscriber の onComplete メソッドを呼ぶ。
  • Subscriber
    • 一つ以上の Publisher を購読して、順序付けされた要素ストリームを受け取る。Producer が Subscriber に要素を渡すときは onNext コールバックを呼ぶ。Subscriber は、Producer をブロックせずに非同期処理するかキューイングする必要がある。
  • Subscription
    • Subscriber から Producer へ要求を伝えるときに使う。Subscriber が Subscription の requestMore(int) メソッドを呼ぶと、Publisher は 制限時間 T が経つ前に最高 N 回まで onNext メソッドを呼び出せる。

API レベルでは、上記の Publisher に対応する型として Producer が、Subscriber に対応するものとして Consumer が定義される。また、両者を組み合わせて入力と出力の両方を行う Processor も提供される。

【追記】組み合わせるとこんな感じ?

誰が関わってるの?

Scala 言語Akka フレームワークの開発をしている Typesafe 社が中心となって仕様策定を進めている。Akka 開発者による解説記事はこちら

それ以外にも、以下のような人々やプロダクトが関わっている。

JVM エコシステムの各方面で実績のある人物・プロダクトが、ズラッと一堂に会しているのが印象的。特に、Oracle の人や Doug Lea 氏が関わっている辺りを見ても、標準化に対する本気が伺える。(元)Microsoft の人が一枚噛んでるのも興味深い。

批判とか

Reactive Streams の”思想的根拠”である Reactive Manifesto に対する批判。たしかに、”そもそも "Reactive" って何だよ” というのがよく分からない感じはある。

ただ、Akka というプロダクトは明らかに Erlang/OTP に対するリスペクトから出てきたものだし、本人達もそれを隠しているわけではない(例えば、Akka の公式サイトの名前は "Let it crash")。アクターモデルを広めたいという意図こそあれ、Erlang の成果を横取りしようとしている、というのは考え過ぎじゃないかなぁと。【追記】: @pokarim さんから「(引用した発言に)そのような意図はない」とご指摘を受けたので訂正します。失礼しました。

まとめ

非同期ストリーム処理フレームワークは乱立気味、というか思い切り乱立しているので、使う側からすると「標準化してくれるのは助かるなぁ」という感想。

とりあえず、これから出てくる実装を開発者が各々の立場から調べて、フレームワークの相互運用性や流量制御といったフィーチャーが、自分たちのユースケースに対してどの程度メリットがあるのか判断していけば良いのではないかと思う。

あと、そういえば Storm に声は掛かってないんだろうか? 明らかにこのモデルに乗っけられそうだけど。

Hadoop/Storm の統合を実現する Twitter の SummingBird

TwitterSummingBird を正式リリースして早二ヶ月。「日本語の紹介記事がほとんど出てないな」と気付いたので、調査がてらまとめてみました。

SummingBird とは?

MapReduce なプログラムを書くための Scala/Java ライブラリ。最大の特徴は、ひとたび SummingBird で書いたジョブは Hadoop でも Storm でも同じように実行できること。

SummingBird では、Hadoop を使う「バッチモード」と、Storm を使う「リアルタイムモード」に加えて、二つを同時に実行する「ハイブリッドモード」がある。ハイブリッドモードでは、ジョブの作者が特に配慮しなくても、バッチとリアルタイムの処理結果を自動的にマージできる

ハイブリッドモードでは、同じジョブを Hadoop と Storm で同時に実行できるので、Hadoop の耐障害性と Storm のリアルタイム性を両立できる。

例えば、Storm からリアルタイムな結果を得ながら、仮に Storm がコケた場合でも、あとから Hadoop が同じデータを処理して結果を復旧できる。また、Storm の苦手な処理(数ヶ月分の過去データを再計算したり、データベースにランダム書き込みしたり)は Hadoop で実行するといったことができる。

ちなみに、こうしたバッチ処理とリアルタイム処理を組み合わせる考え方は、Storm の作者である Nathan Marz (@nathanmarz) 氏が提唱する "Lambda Architecture" に基づいている。このへんにご本人による解説記事があるので、興味がある人はどうぞ。

ロジックの書き方

SummingBird の MapReduce DSL は、基本的には Scala 標準のコレクションライブラリ API に準拠している。例えば、単語の数え上げ (word count) を、普通の Scala コレクションの変換処理として書くとこうなる。

def wordCount(source: Iterable[String], store: MutableMap[String, Long]) =
  source.flatMap { sentence =>
    toWords(sentence).map(_ -> 1L)
  }.foreach { case (k, v) => store.update(k, store.get(k) + v) }

これを SummingBird で書くと以下のようになる。つまり、(1) source から入力される文 (sentence) を toWords で単語に分解して、(2) 各単語を ([word], 1L) のタプルに変換し、(3) sumByKey でキーごとに集約して、store の値に加算したあとに保存する。

def wordCount[P <: Platform[P]]
  (source: Producer[P, String], store: P#Store[String, Long]) =
    source.flatMap { sentence =>
      toWords(sentence).map(_ -> 1L) // (1) + (2)
    }.sumByKey(store) // (3)

一見、なじみ深い SQL ライクな DSL とは雰囲気が違うけど、これも SQL と同様に集合演算の考え方に基づいているので、概念的には大きな差はない。

データストリームとプラットフォームの抽象化

SummingBird の核となるコンセプトは ProducerPlatform だ。

Producerデータストリームを抽象化したものmap, filter, flatMap, merge, leftJoin などのメソッドが定義されており、これらを使ってデータストリームに対する処理を宣言的に記述する。

trait Producer[P <: Platform[P], +T] {
  def merge[U >: T](r: Producer[P, U]): Producer[P, U] = ...
  def collect[U](fn: PartialFunction[T,U]): Producer[P, U] = ...
  def filter(fn: T => Boolean): Producer[P, T] = ...
  def lookup[U >: T, V](service: P#Service[U, V]): KeyedProducer[P, U, Option[V]] = ...
  def map[U](fn: T => U): Producer[P, U] = ...
  def flatMap[U](fn: T => TraversableOnce[U]): Producer[P, U] = ...
  ...
}

なお、Producer のメソッドを呼び出しただけでは何の処理も走らない点に注意。PlatformインスタンスProducer を渡して初めて、データストリームに対する処理が実行される。

Platform は、Hadoop や Storm のようなストリーミング MapReduce ライブラリを抽象化したものPlatformの各インスタンスは、Producerで記述した処理を、特定の MapReduce 処理系で実行する方法を実装している。

trait Platform[P <: Platform[P]] {
  type Source[+T]
  type Store[-K, V]
  type Sink[-T]
  type Service[-K, V]
  type Plan[T]

  def plan[T](completed: Producer[P, T]): Plan[T]
}

例えば、Storm の実装はこうなる。

  type Source[+T]: Spout[(Long, T)]
  type Store[-K, V]: StormStore[K, V]
  type Sink[-T]: (T => Future[Unit])
  type Service[-K, V]: StormService[K, V]
  type Plan[T]: StormTopology

Platform が抽象化されているということは、つまり、SummingBird は Hadoop や Storm 以外のフレームワークにも適用できるということ。実際に、今後のバージョンで SparkAkka などのサポートが予定されている。

関連プロジェクト

SummingBird 自身は一万行程度のライブラリだが、多くの関連プロジェクトの成果の上に成り立っている。いずれも Twitter 自身が開発して OSS 化しており、SummingBird とは独立に利用できる。

  • Algebird: Scala 向けの抽象代数学ライブラリ(後述)。元々は Scala 向け Hadoop ラッパー Scalding の一部だった。
  • Bijection: 異なる型のオブジェクトを相互に変換するライブラリ。型クラスによる拡張をサポートし、異なるプラットフォームやクライアント上でシリアライズ方式を共有できる。
  • Chill: シリアライズライブラリ Kryo のラッパー。Storm、Scala、Hadoop 向けのモジュールを提供しているほか、Spark でも使われている。
  • Tormenta: Storm の Scala 拡張で、Spout に対して型安全性やマッピング、フィルタリング等の機能を提供する。
  • Storehaus: キーバリューストアに対する非同期クライアントを実装するためのライブラリ。(SummingBird における)Storm のリアルタイム集計処理は Storehaus の MergeableStore トレイトを使っており、Memcached や Redis などに対応している。

モノイドと並列処理

最初の紹介で「バッチとリアルタイムの処理結果を自動的にマージできる」と書いたが、この特性を支えるのが、上で紹介した Algebird が提供するモノイド半群の実装だ。

モノイドは、我々が日常的に使う「足し算」や「掛け算」の概念を抽象化したもので、結合律(と単位律)を満たす「足し算のようなもの」を総称する概念だ。「のようなもの」というのは、足されるのは「数」とは限らないから。身近な例では「リストの結合」もモノイドだ。

いきなり「結合律」と言われても何のこっちゃという感じだが、例えば、

a1 + a2 + a3

という足し算があったとしよう。これを (a1 + a2) + a3 という順番で足しても a1 + (a2 + a3) という順番で足しても、結果は変わらない。これが結合律。

つまり、この性質を満たす演算はどこからどの順番で足し合わせても同じ結果になるので、並列処理と相性がよい。また、その結果として、オンライン処理の結果とオフライン処理の結果も簡単にマージできる。

Algebird は、データ分析用途に使えるモノイドや半群の実装を多数提供している。例えば、HyperLogLogBloomFilter のようなアルゴリズムを SummingBird でそのまま利用できる。

trait Monoid[V] {
  def zero: V
  def plus(l: V, r: V): V
}

ユーザが自分のモノイドを実装することもできる。やることは Monoidzeroplus を実装して、あらゆる値で結合律を満たすことを確認するテストを書くだけ。

assertEquals(m.plus(a, m.plus(b,c)), m.plus(m.plus(a,b), c)

【11/15追記】 モノイドの自作については、yukitos さんが翻訳されている "Understanding monoids" シリーズがとても参考になります。

まとめ(というか余談)

この半年ほど、色々な Web 企業が公開している OSS プロダクトのソースコードを調べる機会があったのですが、個人的には、エンジニアリングの水準は Twitter が頭一つ抜けているなぁという印象を持っています。

Twitter 製 OSS プロダクトのレベルの高さを感じるのは、コードがよく整理されていて再利用性が高いのが一つ。もう一つは、アカデミックな知見の応用に積極的に取り組んでいる点です。

それも、単に学問的な純粋さを追求するのではなく、抽象化のレベルが適切で、理論と実践のバランスが注意深くはかられています。Tumblr や LinkedIn、Pinterest などの企業で Twitter 製プロダクトが続々と採用されているのも、開発生産性に加えて性能面での優秀さも一因でしょう。

あと、いち Scala ファンとしては、データ分析分野での Scala 活用事例としても注目しています。

なぜ SummingBird は、従来の SQL ライクな記法ではなく Scala DSL を採用したのか。その理由は今のところ特に語られていませんが、型安全な API や、型クラスによる拡張性の担保、あるいは非同期処理の実装を容易にする Future の活用など、SummingBird では Scala の機能がうまく活かされていると思います。

Scala 製分散データ処理フレームワークの代表格である Spark の方も、Databricks 創業や Cloudera との提携 といった明るいニュースが続いていますし、「データ分析用言語としての Scala」が盛り上がってきている感があります。この調子で、今後の躍進を期待したいところです。

参考文献

翻訳: "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 を使え”と言ったばかりである…。

Java 8を関数型っぽく使うためのおまじないをScalaで(ry

はじめに

言うまでもなく下記の記事のパクリです。JavaC# と F# があるのに Scala が無いなんて、と謎の使命感に駆られた結果がこれだよ!

まずは、素晴らしい記事を公開して頂いたご両名に感謝を。というか、こういう記事をさらさらと書き下せる方は本当にすごいと思いました。ぼくにはとてもできない。

Function1 型

Java 8 ではFunction型が追加されましたが、Scalaオブジェクト指向と関数型のハイブリッドなので、最初からFunction1という型があります。パッケージ名や型引数まで含めると scala.Function1[-T1, +R] です。T1を引数に取ってRを返す関数、と読みます。

こんな感じで使います。

// Function<String, String> enclose = s -> "[" + s + "]";
val enclose: Function1[String, String] = s => "[" + s + "]"

Stringを引数にとってStringを返すencloseという関数を定義しました。

また、Scala には型推論があるので次のようにFunction1型の宣言を省略できます。

val enclose = (s: String) => "[" + s + "]"

この関数に引数を与えて呼び出すと [foo] と表示されます。

println( enclose("foo") )

もうひとつ関数を定義してみましょう。最初の文字を大文字にする関数です。ちなみに、ScalaJVM 言語なので Java の機能をそのまま使えます

// Function<String, String> capitalize = s -> 
//     s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
val capitalize = (s: String) =>
  s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase()

同様に引数を与えて呼び出すとFooと表示されます。

println( capitalize("foo") )

この二つを順に呼び出す、つまりcapitalizeしてencloseすると[Foo]と表示されます。

// System.out.println( enclose.apply(capitalize.apply("foo")) );
println( enclose(capitalize("foo")) )

こういう場合、Java と同様にandThenメソッドを使うと二つの関数を連結できます。Scala は中置記法が使えるので、可読性を下げるカッコを減らすことができます

// System.out.println( capitalize.andThen(enclose).apply("foo") );
println( (capitalize andThen enclose)("foo") )

これを関数合成といいます。合成した関数に新しい名前を与えて使うこともできます。

val capEnc = capitalize andThen enclose
println( capEnc("foo") )

Scala で関数型っぽいことができることがわかりました。やりましたね!

関数の定義

ところで、ここまで関数をFunction1型のインスタンスとして扱ってきましたが、defを使って明示的に関数として定義することもできます。この場合、引数は Java と同様にfunc(a: A)の形式で書きますが、書き方はvalで定義する場合とほとんど同じです。

// enclose: String => String
def enclose(s: String) = "[" + s + "]"
// capitalize: String => String
def capitalize (s: String) =
  s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase()

同様に関数合成もできます。やはり、Function1型の時と同じ記法が使えることが分かります。

// capEnc: String => String
def capEnc = (capitalize _) andThen enclose
println( capEnc("foo") )

別名を使う

Scala では、Function1[-T1,+R]の別名としてT1 => Rが使えるのでおまじないは要りません

val enclose: String => String = s => "[" + s + "]"

ところで、1 引数のFunction1[-T1,+R]があるってことは、2 引数のFunction2[-T1,-T2,+R]や 3 引数のFunction3[-T1,-T2,-T3,+R]も同様にあるんじゃないかと思うかもしれませんが、2 引数以上の関数は甘えなので、1 引数の関数さえ知っていれば問題ありません。

関数合成

先ほども述べたように、関数合成は以下のように書けます。

println( (capitalize andThen enclose)("foo") )

さらにもう一つ関数を定義して次のように書いてみます。まんなかあたりを取り出す関数middleです。

val middle = (s: String) => s.substring(s.length() / 3, s.length() * 2 / 3)

// println._( middle.x(capitalize).x(enclose)._("yoofoobar") );
println( (middle andThen capitalize andThen enclose)("yoofoobar") )

関数合成を使わないと、次のようになりますね。

// println._( enclose._(capitalize._(middle._("foobaryah"))));
println( enclose(capitalize(middle("foobaryah"))) )

このように、実際に呼び出す順と記述が逆になります。最初にmiddleして、次にcapitalizeして、最後にencloseしたいのに、まずencloseから書く必要があります。

また、カッコがたまっていくので、最後にまとめてカッコを閉じる必要があります。ちょっと関数呼び出しが増えると、閉じカッコの数がよくわからなくなってコンパイルエラーがなくなるところまで「)」を増やしたり減らしたりなんてこと、ありますよね。

Function1に次のようなメソッドを追加しておくとさらにいいです。

implicit class Function1Ops[-T1,+R](val f: T1 => R) extends AnyVal {
  def して[R2](g: R => R2): T1 => R2 = f andThen g
  def するのを(a: T1) = f(a)
}

こうなって読みやすいですね。

// println._( middle.して(capitalize).して(enclose).するのを("yoofoobar") );
println( capitalize して enclose して middle するのを "yoofoobar" )

…読みやすいですね?

カリー化

さて、2 引数以上の関数は甘えと書きましたが、実際 2 つ以上のパラメータを渡したいときはどうすればいいんでしょう?

こういうときに使うのがカリー化です。カリー化は、ひとつの引数をとって関数を返すことで、複数のパラメータに対応します。

例えば、挟む文字挟まれる文字を指定すると文字を文字で挟む関数sandwichを、通常の 2 引数関数で表すとこうなります。

// String sandwich(String enc, String str){
//     return enc + str + enc;
// }
def sandwich(enc: String, str: String) = enc + str + enc

これを 1 引数関数でカリー化して書くと次のようになります。

/// F<String, F<String, String>> sandwich = enc -> str -> enc + str + enc;
val sandwich: String => String => String = enc => str => enc + str + enc;

sandwich自体は、文字列を引数に取って、”文字列を引数に取って文字列を返す関数” を返す関数になっています。

この関数に 2 引数を与えて呼び出すと***sanded!***と表示されます。

// println._( sandwich._("***")._("sanded!") );
println( sandwich("***")("sanded!") )

3 引数だとこんな感じですね。

// F<String, F<String, F<String, String>>> encloseC = pre -> post -> s -> pre + s + post;
val encloseC: String => String => String => String = pre => post => s => pre + s + post

encloseCは、文字列を引数に取って、”文字列を引数に取って、”文字列を引数に取って文字列を返す関数” を返す関数” を返す関数になっています。

この関数に 3 引数を与えて呼び出すと{enclosed!}と表示されます。

// println._( encloseC._("{")._("}")._("enclosed!") );
println( encloseC("{")("}")("enclosed!") )

ところで、このカリー化されたencloseC、引数を部分的に渡しておくことができます。

// F<String, String> encloseCurly = encloseC._("{")._("}");
val encloseCurly = encloseC("{")("}")

println( encloseCurly("囲まれた!") )

こうやって部分適用することで、新しい関数が作れるわけです。ちなみに Curly は波カッコ=カーリーブラケット{}のことで、カリー化とは関係ないのであしからず。

さて、同じように 4 引数だと…え、もう=>は書きたくない?

大丈夫、Scala はそんなあなたのためにcurriedメソッドを用意しています。これを使えば、2 引数以上の関数をカリー化された関数へと簡単に変換できるのです。curriedは 2 引数以上のあらゆる関数で使えるので安心!

// fourArgFunc: String => String => String => String => String
val fourArgFunc = ((a: String, b: String, c: String, d: String) => a + b + c + d).curried
println( fourArgFunc("a")("b")("c")("d") )

2 引数以上の関数は甘え? 何のことですか?

まとめ

以上をまとめて書くと、こんな感じです?

ちなみに、はてなの markdown 記法は Scala に対応してるっぽくて素晴らしい。

val enclose = (s: String) => "[" + s + "]"
println( enclose("foo") )
val capitalize = (s: String) => 
    s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase()
println( capitalize("foo") )

//関数合成
println( (capitalize andThen enclose)("foo") )

val middle = (s: String) => s.substring(s.length() / 3, s.length() * 2 / 3)
println( (middle andThen capitalize andThen enclose)("yoofoobar") );

println( enclose(capitalize(middle("foobaryah"))))
println( middle して capitalize して enclose するのを "yoofoobar" )

//カリー化
val sandwich: String => String => String = enc => str => enc + str + enc;
println( sandwich("***")("sanded!") )

val encloseC: String => String => String => String = pre => post => s => pre + s + post
println( encloseC("{")("}")("enclosed!") )

val encloseCurly = encloseC("{")("}")
println( encloseCurly("囲まれた!") )

val fourArgFunc = ((a: String, b: String, c: String, d: String) => a + b + c + d).curried
println( fourArgFunc("a")("b")("c")("d") )

おまけ

関数型プログラミングの技法を関数型言語で書いた方が自然に書けるのは当たり前です。どこかで読んだ話によれば、Java 8 のラムダ式は関数型プログラミングのサポートが第一目的ではないとのことで、ならば、できないこともそれなりにあって当然です。

とはいえ、ラムダ式の導入によって Java プログラマにとっても関数型プログラミングの考え方や方法論がグッと身近になるのも事実。

というわけで、この機会に少し関数型プログラミングの考え方を勉強してみるのはいかがでしょうか? たとえ実際の仕事に使う機会がなくとも、あなたのプログラミングの芸風を広げるのにきっと役立つはずです。

おまけ2

関数型プログラミングへの導入に役立ちそうなポインタをいくつか。

まず、IIJ の山本和彦さんが Java プログラマ向けの関数型プログラミングの紹介記事 ”他の言語を学んで自由になろう” を公開されています。

また、手前味噌ですが Java プログラマ向けの Scala 紹介スライドを公開しています。このスライドでは関数型プログラミングについての話題はほとんど省いていますが、もし Scala という言語自体にご興味があればどうぞ。