マイクロサービスが Scala を選ぶ3つの理由
今年も開催される Scala Advent Calendar 2014 の 15 日目にエントリーしていて、ネタとしては先日 Tumblr が発表した "I/O and Microservice library for Scala" を謳う Colossus をやる予定なんだけど、前振りとして「なぜマイクロサービス化を進めるサービスは Scala を選ぶのか」という話をしてみるエントリ。ちなみに、Advent Calendar の前振りと書いたけど、とりあえず Scala をあまり知らない人向け。
そもそもマイクロサービスって何だっけ?
この記事とかよくまとまってると思います。
マイクロサービスへの移行と Scala
成功を収めたウェブサービスが、自身のビジネスを持続的にスケールさせるため、巨大化・複雑化したモノリシックなアプリケーションを解体し、単機能のコンポーネントであるマイクロサービスからなるサービス指向アーキテクチャ (SOA) へと移行する…。近年、こんなストーリーを耳にする機会が多い。
この際、ランタイムをスクリプト言語から JVM へ置き換えたり、特に使用言語として Scala を採用するケースが目立つ。以下は一例だが、名だたる有名サービスが Scala を使ったマイクロサービス化に取り組んでいることがわかる:
- Twitter:
- Tumblr:
- SoundCloud:
- ドワンゴ:
なぜ Scala が選ばれるのか?
なぜ、マイクロサービス化で Scala が選ばれるのか? Scala 移行の事例を見ると、次の三つのポイントが指摘されることが多い。
以下に一つずつ見て行こう。
1. JVM 言語である
We were enamored by the level of performance that the JVM gave us. It wasn’t going to be easy to get our performance, reliability, and efficiency goals out of the Ruby VM, so we embarked on writing code to be run on the JVM instead. We estimated that rewriting our codebase could get us > 10x performance improvement, on the same hardware –– and now, today, we push on the order of 10 - 20K requests / sec / host.
近年、様々なサービスが、自社サービスの再構築にあたって、大規模なトラフィックに耐える性能・信頼性・効率性の要件を達成するために Java VM (JVM) を選んだと証言している。JVM は、過去 10 年以上にも渡ってサンやオラクル等によって大きな開発リソースが投じられてきたこともあって、高い性能と安定性を実現している。
つまり、JVM 言語として作られた Scala は、当然その恩恵を受けることができる。
また I/O 性能についても、JVM は VM 内のプログラムから OS の低レベル I/O を直接叩ける NIO (Non-blocking I/O) API を備えている。よって、クライアント−サービス間に加えて、内部サービス間の I/O 性能が特に重要となるマイクロサービス・アーキテクチャとも相性が良い。
また、採用面で技術者の確保が容易である点もしばしば挙げられる。
Changed to a JVM centric approach for hiring and speed of development reasons.
高負荷環境での JVM の運用は GC(ガベージ・コレクション)との戦いになると言われるが、他の言語ランタイムに比べて、そのあたりのチューニングのノウハウを持った人材を確保しやすいということもあるのかもしれない。
2. Finagle の存在
Finagle was a compelling factor in choosing Scala. It is a library from Twitter. It handles most of the distributed issues like distributed tracing, service discovery, and service registration. You don’t have to implement all this stuff. It just comes for free.
Scala 採用の理由として、Twitter が開発した Scala 製非同期 RPC フレームワーク "Finagle" の存在を挙げるサービスは多い。Finagle の興味深い点はたくさんあるが、マイクロサービスのためのフレームワーク、という観点から言うと以下の三つ(性能、プログラミングモデル、運用ツールとの連携)が挙げられる。
性能
Finagle は、先に挙げた NIO フレームワークの定番である Netty の Scala ラッパーであり、高い性能を誇る。また、Twitter 自身が Netty の開発に大きくコミットしており、Finagle の開発とも密接に連携している。
Finagle の高性能は、”バルス祭り”の大規模トラフィックにも耐えうるシステムの構築に大きく貢献した。
Our new stack has enabled us to reach new records in throughput and as of this writing our record tweets per second is 143,199.
プログラミングモデル
マイクロサービス・アーキテクチャでは、各サービスの実装にあたって、必然的に他の内部サービスに対する非同期 RPC のコーディングが必要になるが、このスタイルには厄介な点がいくつもある。
- ある RPC の結果を使って次の RPC をリクエストするような逐次処理や、並列にリクエストした複数の RPC の結果が揃うのを待って集約するような並列処理を書こうとすると、どうしてもコードが煩雑になる。
- リクエストした処理が長時間戻ってこない場合、スレッドをブロックしないようにプログラムする必要がある。
- あらゆるリモートへのリクエストは、想定外の理由で失敗する可能性がある(ネットワーク障害、リモートホストの障害、等々)。したがって、エラー処理やリトライ、タイムアウト等を考慮したコーディングが必要になる。
Finagle は、非同期 RPC を Future と Service/Filter というインタフェースで抽象化する。これらの API は、オブジェクト指向言語であると同時に関数型言語である Scala の特徴をよく活かしたものになっている。
Future
と Service
、そして Filter
を組み合わせると、例えば、以下のようにマイクロサービスを組み合わせてサービスを作る際に、逐次処理と並列処理が絡み合った複雑な非同期 RPC 処理をクリーンかつシンプル、そして安全に記述できる(引用元)。
- 認証サービス (AuthService) に問い合わせて、ユーザ認証を行う
- タイムラインサービス(TimelineService) に問い合わせて、指定したユーザの Tweet ID の一覧を受け取る
- ツイートサービス (TweetService) に各 ID に対応するツイート本文を並列に問い合わせて、全ての結果が戻ってきたら集約してクライアントに返す
val timelineSvc = Thrift.newIface[TimelineService](...) // #1 val tweetSvc = Thrift.newIface[TweetService](...) val authSvc = Thrift.newIface[AuthService](...) val authFilter = Filter.mk[Req, AuthReq, Res, Res] { (req, svc) => // #2 authSvc.authenticate(req) flatMap svc(_) } val apiService = Service.mk[AuthReq, Res] { req => timelineSvc(req.userId) flatMap { tl => val tweets = tl map tweetSvc.getById(_) Future.collect(tweets) map tweetsToJson(_) } } //#3 Http.serve(":80", authFilter andThen apiService) // #4 // #1 サービスごとのクライアントを作成する // #2 入ってくるリクエストを認証する Filter を作成する // #3 認証されたタイムラインリクエストを JSON に変換して返す service を作成する // #4 認証 filter と service を使って 80 番ポートで動作する HTTP サーバを開始する
ここではエラー処理が明示的に書かれていないが、上記の処理のいずれかが失敗した段階(認証が失敗した場合とか、タイムラインサービスへの問い合わせがタイムアウトした場合とか)で処理全体が失敗するようになっている。もちろん、明示的に復旧処理を書くこともできる。
Service
の実装は一般的な HTTP (REST) だけではなく Thrift も使える*1し、Memcached や MySQL、あるいは Redis などのデータベース向けプロトコルも用意されている。
このように様々なプロトコルを Service
として抽象化しているため、アプリケーション非依存な機能である Filter
を様々なプロトコルに対して直交的に適用できる。Filter
で追加できる機能には、認証やリトライ、そして後述するモニタリングやトレースといったものがある。
運用ツールとの連携
RPC ベースの分散システムへ移行する際の厄介事は、運用・監視の面でもたくさんある。例えば、以下のうちいずれを欠いても、多数のノードで構成される複雑なマイクロサービス群の運用やデバッグは難しくなる。
また、個々の開発者が、担当するマイクロサービスに対してこれらのツールを簡単に組み込める必要がある。
Finagle には、ZooKeeper や分散トレースシステムの Zipkin 等と連携する機能が最初から組み込まれている。つまり、必要な設定を追加してやるだけで、ZooKeeper を使ってクラスタを組み、リクエスト数などのメトリクスをリアルタイムに監視し、各ノードでリクエストの処理にかかった時間を Zipkin へ自動的に集約して可視化したりできる。
3. 静的型付き言語である
運用が必要なシステムで1万、2万行越えだすと、静的型付けであることによる保守性の高さは、結果的にコンパイル時間等を払拭できるほどの安全性、生産性、心の平安を生むと思っていて、要は静的型付けである事は非常に価値があって、特にテストが難しいテンプレート(twirl)の静的型付けは素晴しいという事を言いたかった。
マイクロサービス化の究極的な目的は、ビジネス要求の変化に合わせて継続的にサービスを更新できる体制を作ることにある。つまり、Scala の静的型付け (static typing) が提供する保守性(≒安心してコードを書き換えられる性質)は、常に変化を必要とするようなシステムでこそ大きなメリットがある。
余談
後日に紹介予定の Colossus は、Tumblr が Finagle に相当するものを自分たちの要件に合わせて作ったものだと考えると良さそう。
また、Finagle が Netty ベースであるのに対し、Colossus は Scala 言語の開発元が作っている Akka(アクターモデルの分散フレームワーク)ベースで、さらに Akka 自身が Netty に相当するレイヤ (Akka I/O) の再実装を進めていたりと、そうした「フレームワーク同士の競争」という野次馬的な面でも興味深い。
Future/Promise はいつモナドになったのか
「非同期計算をモナドで合成し、依存関係に従ってパイプライン化する」というアイデアはいつ誰が提案したのか、というのを調べてみたけどよく分からなかった記録。網羅的な調べ方はしてないので、何か知ってる人がいたら教えてください。
明示的 vs. 暗黙的
id:xuwei さんに教えて頂いた Wikipedia の記事によると「まだ完了していない計算結果へのプロキシオブジェクト」というコンセプトが Future や Promise と名付けられたのは 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 では、先行例として MochiKit や Dojo's Deferred を挙げている。少なくとも 2006 年頃にはあった模様。Twisted の Deferred は「明示的」なスタイルなのでちょっと違うかな。
最近は、Promises/A+ 仕様として標準化されて Thenable と呼ばれている。
Future/Promise モナド
結論から言うと、少なくとも Scala の実装においては、「Future/Promise はモナドである」というようなことを書いてる論文等があったり、それを参照して実装したりした、というわけではなさそう。
まず、Akka において公開レポジトリで辿れる最古の Future 実装を見ると完全に「明示的」なスタイルで、map
も flatMap
も見当たらない。
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 モナド?
追記
@okapies あとHaskellのContinuation Monadはたぶん違くて(あれはたしか並列並行は扱わない)、自分も詳しくないけど、少し調べた感じこういうの http://t.co/AOA5MblWFO が、ある程度近いものな気はします
— Kenji Yoshida (@xuwei_k) 2014, 11月 23
この Par
モナドに成功/失敗の文脈を組み合わせると近いのかな。
追記2
@xuwei_k @okapies Haskellでfutureのように非同期処理を組み合わせられるもので広く使われているものといえばasyncパッケージだと思います。Concurrentlyを使うと似たような感じになります。 https://t.co/tX0W8ao0OV
— Mitsutoshi Aoe/maoe (@ma0e) 2014, 11月 23
@xuwei_k @okapies hackageのものはConcurrentlyがMonadのインスタンスになっていませんが、githubのmasterではなっています。
— Mitsutoshi Aoe/maoe (@ma0e) 2014, 11月 23
@xuwei_k @okapies Applicativeで並行に実行して、Alternativeでは並行に実行して早く返ってきた方の結果を返す、Monadでは逐次実行になります。
— Mitsutoshi Aoe/maoe (@ma0e) 2014, 11月 23
@okapies @ma0e 「Applicativeで並行」と「Monadでは逐次実行」は、Scalazでも、その中にあるScala標準のFutureのインスタンスに対してはそうなってますね。
ただ、そこの細かい動作についてよく議論になったり、別の型クラス作られたりしてますが
— Kenji Yoshida (@xuwei_k) 2014, 11月 24
*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) }
^^;;;
考察
Mathematica、画像や動画の読み書きをネイティブサポートしているのはホントにいいなぁ、という感想。
なんか、動画を扱える Pure Java の成熟したソリューションって未だにあんまりないらしくて、したがって OpenCV を Java から叩くのが一番堅実な選択肢ということになり、その道の先には jar をソースからビルドする楽しい作業が待ってたり、コーディング時もリソースの明示的な解放をサボると一瞬でメモリが爆裂したりと、色々めんどくさい…。
コード
今回書いたコードは GitHub に置いておきますので、世紀末ごっこして遊ぶなり改造するなりどうぞ。
あと、mean のアルゴリズムはもう少しマシな方法があるように思うので、OpenCV に詳しい方、どなたかご教示くださいませ…。
追記
- Webカメラ映像から人を消す(colspan's blog)
非同期ストリーム処理の標準化を目指す "Reactive Streams" とは
TL でこんなのが流れてたので少し調べてみた。
Learn about the Reactive Streams initiative & how we're supporting a standard for asynch stream processing on the JVM http://t.co/5wUF0PjTBe
— Twitter Engineering (@TwitterEng) 2014, 4月 17
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 をブロックせずに非同期処理するかキューイングする必要がある。
- 一つ以上の Publisher を購読して、順序付けされた要素ストリームを受け取る。Producer が Subscriber に要素を渡すときは
- Subscription
- Subscriber から Producer へ要求を伝えるときに使う。Subscriber が Subscription の
requestMore(int)
メソッドを呼ぶと、Publisher は 制限時間 T が経つ前に最高 N 回までonNext
メソッドを呼び出せる。
- Subscriber から Producer へ要求を伝えるときに使う。Subscriber が Subscription の
API レベルでは、上記の Publisher に対応する型として Producer が、Subscriber に対応するものとして Consumer が定義される。また、両者を組み合わせて入力と出力の両方を行う Processor も提供される。
【追記】組み合わせるとこんな感じ?
So glad ! http://t.co/tUFxBpfYrF is up !akkaStream.produceTo(rxjavaObservable).produceTo(reactorStream).produceTo(vertxStream) !
— Stephane Maldini (@smaldini) 2014, 4月 17
誰が関わってるの?
Scala 言語と Akka フレームワークの開発をしている Typesafe 社が中心となって仕様策定を進めている。Akka 開発者による解説記事はこちら。
それ以外にも、以下のような人々やプロダクトが関わっている。
- Gavin Bierman: Oracle の人で Java や JVM に関わってるっぽい。
- Jon Brisbin, Stephane Maldini: Pivotal Software 所属で、要するに Spring Framework のひと。Spring IO の基盤である "Reactor" の開発者。Reactor についてはこの辺に日本語記事がある。
- Mathias Doenitz, Johannes Rudolph: Akka を使った HTTP サーバ "Spray" の開発者。現在は Typesafe 傘下で、Akka 自体の開発にも携わっている(はず)。
- George Campbell, Ben Christensen: Netflix が開発している JVM 向けの Rx (Reactive Extensions) 実装である "RxJava" の開発者。
- Marius Eriksen: Twitter の非同期 RPC フレームワーク "Finagle" の開発者。今のところドキュメントに Finagle の名前は見えないけど、将来的には実装を提供していくのだろうか。
- Tim Fox, Norman Maurer: 非同期アプリケーションフレームワーク "Vert.x" の開発者で、現在は Redhat 所属。Norman は、Java のイベント駆動フレームワークとして広く使われている Netty の主要開発者でもある。Vert.x と Reactive Streams の関係についてはこの記事で解説している。
- Doug Lea: ”Java並行処理プログラミング”の筆者の一人。また、java.util.concurrent (JSR-166) の作者として著名な人物。
- Erik Meijer: Microsoft で LINQ や Reactive Extensions に関わっていたらしい。現在は退職して Applied Duality という会社を立ち上げている。
JVM エコシステムの各方面で実績のある人物・プロダクトが、ズラッと一堂に会しているのが印象的。特に、Oracle の人や Doug Lea 氏が関わっている辺りを見ても、標準化に対する本気が伺える。(元)Microsoft の人が一枚噛んでるのも興味深い。
批判とか
Reactive Streams の”思想的根拠”である Reactive Manifesto に対する批判。たしかに、”そもそも "Reactive" って何だよ” というのがよく分からない感じはある。
ただ、Akka というプロダクトは明らかに Erlang/OTP に対するリスペクトから出てきたものだし、本人達もそれを隠しているわけではない(例えば、Akka の公式サイトの名前は "Let it crash")。アクターモデルを広めたいという意図こそあれ、Erlang の成果を横取りしようとしている、というのは考え過ぎじゃないかなぁと。【追記】: @pokarim さんから「(引用した発言に)そのような意図はない」とご指摘を受けたので訂正します。失礼しました。
まとめ
非同期ストリーム処理フレームワークは乱立気味、というか思い切り乱立しているので、使う側からすると「標準化してくれるのは助かるなぁ」という感想。
とりあえず、これから出てくる実装を開発者が各々の立場から調べて、フレームワークの相互運用性や流量制御といったフィーチャーが、自分たちのユースケースに対してどの程度メリットがあるのか判断していけば良いのではないかと思う。
あと、そういえば Storm に声は掛かってないんだろうか? 明らかにこのモデルに乗っけられそうだけど。
Hadoop/Storm の統合を実現する Twitter の SummingBird
Twitter が SummingBird を正式リリースして早二ヶ月。「日本語の紹介記事がほとんど出てないな」と気付いたので、調査がてらまとめてみました。
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 の核となるコンセプトは Producer
と Platform
だ。
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 以外のフレームワークにも適用できるということ。実際に、今後のバージョンで Spark や Akka などのサポートが予定されている。
関連プロジェクト
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 は、データ分析用途に使えるモノイドや半群の実装を多数提供している。例えば、HyperLogLog や BloomFilter のようなアルゴリズムを SummingBird でそのまま利用できる。
trait Monoid[V] { def zero: V def plus(l: V, r: V): V }
ユーザが自分のモノイドを実装することもできる。やることは Monoid
の zero
と plus
を実装して、あらゆる値で結合律を満たすことを確認するテストを書くだけ。
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 のサンプルコード。
UserModule
とTweetModule
がある。UesrModule
はloadUser()
でユーザを読み出す。MySQLUserModule
はUserModule
を実装している。TwitterModule
は、TweetModule
とUserModule
の両方を使う(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
は、MySQLUserModule
とTwitterModule
を組み合わせる。コンパイラが、列挙された依存関係にloadUser
とpost
が実装されているか確認してくれる。
型理論 (Type Theory)
状態の隠蔽
- private フィールドの
x
と、x
と引数y
を使って新たな値を返す関数add
を持つクラスFoo
を考える。
class Foo(x: Int) { def add(y: Int) = x + y }
- ここで
add
は、”Foo
オブジェクト内に隠蔽された状態x
”を暗黙的な第一引数として受け取る関数、とみなすことができる。
型の隠蔽
- 以下のコードで、
apply
関数は型パラメータA
をとる。これは、全てのA
で正当化される。apply
にA
を与えるとOption[A]
が得られる。
trait Forall { def apply[A](a: A): Option[A] }
- ここでは型シグネチャに注目する。これは (for all) だ。論理学を知ってる人なら、全称量化 (universal quantification) と存在量化 (existential quantification) についても知っていると思う。
- 全称量化()は、 全ての について が真であると言っている。
- 存在量化()は、 ある について が真であると言っている。僕らは が何であるか知っている。
- つぎに (for some) を Scala で表すと以下のようになる。
trait Exists { type A def apply(a: A): Option[A] }
Exists
トレイトでは、A
はapply
のパラメータではなくExists
の内側にある型になっている。- このとき、以下のように
Exists
のapply
メソッドを呼んでもうまくいかない。
def bippy(e: Exists) = e(42)
- このコードがエラーになる理由:
- これを型シグネチャで表すと ではなく となる。
- つまり、ある が存在するとき、 がいつでも存在する。
- この型シグネチャはいくつかの情報を捨てているが、同時に を使って何かすることを示している。
- なぜ、開発者に対してクラスが持つ型を隠すことが有用なのか?
- 情報の隠蔽。 それが、モジュール化の本質であり、オブジェクト指向の中核的な原則だから。
- コードベースの複雑さを管理するには、情報を分割して、コードベースのある部分が他の部分の詳細を知らないようにし、独立に改良できるようにする必要がある。
存在型
(訳注: Scala で存在型といえばforSome
のことだが、ここでは上述の存在量化を指している。また、 に対応する普通のジェネリクスは普遍型 (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
は状態であると考えることができる。X
はInt
かもしれないし、他の何かかもしれないが、それをクラスの外側から知ることはできない。- コンストラクタのように渡すことで、状態を作ることができる。また、その状態を使って
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
を使ったり渡したりして最終的な結果を生成できる。- 情報隠蔽と存在型を使ったモジュール化のコンセプトは、それについてあまり深く考えたことがないだけで、僕らが毎日のようにやっていることだ。
- Cake Pattern を使うと、存在型を使った情報隠蔽のコンセプトを活用できる上に、もっと強力なモジュールを手に入れることができる。
モジュール性
- Cake Pattern の本質はモジュールで、さらにトレイトはモジュールだ。
- この講演の残りの時間では、パッケージを使わないことにしたい。
- パッケージはひどいし、使うべきではない。パッケージは、名前につくファンシーな接頭辞でしかないし、プログラミングの道具としてあまり良いものじゃない。
- トレイトこそ真のモジュールだ。組み合わせられるし、管理できる。
- パッケージと違い、トレイトは型検査される依存関係 (type-checked dependency) を明示的に記述することができる。
- パッケージも import に失敗したらエラーが出るので、型検査される依存関係を提供しているように見える。だけど、パッケージの外側を見ることができないし、import したものが全て見えてしまう。
- また、パッケージ同士を組み合わせることができないし、特定の方法で import した元のパッケージの中で import されたものを変更することもできない。
- 同様に、トレイトは完全なカプセル化を提供する。
- Scala のような言語では、この方法でトレイトを使うことでオブジェクト指向プログラミングができる。
- 僕らは関数型プログラミングから得られる利益を知っているし、僕も Haskell 的な高階関数スタイルのプログラミングや抽象化を使うことが多い。それはとても素晴らしいし重要なんだけど、とても巨大な論理的モジュールを構造化する時には、僕は Cake Pattern を使ったオブジェクト指向へと立ち戻る。
- Cake Pattern は実務でとても役に立つし、今や、大規模なコードベースで実践済みだ。問題もあるが、全体としては驚異的に有益な経験だ。
- では、君のコードベースに Cake Pattern を配備するプロセスを、三つのステージで考えてみよう。
Stage 1: 導入
- ステージ 1 は Cake Pattern の導入。
- まず、君のコードベースをトップレベルのトレイトに分割する。
- 基本的には、一つのトレイトごとに一つのファイルを用意するといい。各ファイルはトレイトに関する1以上のレベルを意図しており、それで全てだ。
- これだけでも、多くの利益が得られる。
- ファイル A の中にあるものにアクセスするには、そのトレイトをファイル B の中にミックスインする必要がある。
- 今や君は、明示的にドキュメント化され、型チェックされた A - B 間の依存関係を手にしている。以前は、依存関係が暗黙的だったのと対称的だ。
- モジュールとしてのトレイトは
import
の代わりに、継承階層であるextends
とwith
を使う。 - 普通のファイルと違い、トレイト内では”裸の関数”を使うことができる(本当はトレイトのメンバだが、”Cake の中”では裸の関数として扱える)。
- 裸の関数を使うのは、時にとても素晴らしい。なぜなら、JVM の奇妙な癖を回避するため、関数をラップするためだけに無意味に関数をクラスの中に置くことがあるから。
- 全てをトレイトの基底スコープに置く必要はない。名前衝突が起きた時は、内部オブジェクトを使って名前空間を分ける、
例:
UserModule
の中には二つの関数login
とsave
とクラス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
が関心があるのはlogin
とsave
だけなのだから、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
を継承するように制約する。UserLike
はUser
のシグネチャを定義する抽象クラスだ。UserLike
にはid
とname
、そしてsave
関数がある。 - さらに、
UserLike
はUser
へ制約する自分型 (self-type) を持つ。この場合、何も関数が実装されてないので意味はないが、自分自身 (this) を返す関数を定義する時に必要になる。 - 一番下にあるのは、
User
の抽象コンストラクタだ。id
とname
を取って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 の仕様書を読んでabstract
とoverride
を一緒に使えると分かったとき、これは 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
節でMongoUserModule
がMessageModule
の前に来るか後に来るかの順序に依存する。 - ライフサイクルの起動ルーチン(あるいは停止ルーチン)間に依存関係を持つべきでないということは、重要なので覚えておいてほしい。あるモジュールが、それより先に開始する他のモジュールを必要とするなら、ファクトリをスタックに積む等の方法を使う必要がある。両者を単純にスタックに積んで、それが動作するように祈ることはできない。
- しかし一般的には、ほとんどのモジュールは自己完結しているので、こういったライフサイクルをスタックに積むパターンはとてもうまくいく。
Stage 3: 合成とネスト化
- 継承(
with
やextends
)によるインポートはとても良いものだし、予期しない制限もないので使うのをやめる必要はない。 - しかし、継承より合成 (composition) を使うべき場合はある。モジュールが”状態を持った何か”に依存していて、その状態を Cake の外側や、またはそのモジュールの他のインスタンスと共有する必要がある場合は、
with
やextends
による継承を使うことができない。代わりにモジュール内でval
を使う。 - もう一つは、モジュール内にモジュールがネストしている場合だ。
- パッケージの中にあるパッケージは、単に名前に接頭辞を追加するだけだ。
- トレイトは真のモジュールだ。これらは互いにネストできるが、これは単なる接頭辞の付与ではなくポリモーフィズムだ。
- この方法で、独立したライフサイクルを持つことができる。
- 外側のトレイト(=モジュール)が自身の
startup
とshutdown
を持つとき、外側のモジュールの生存期間中に、その内側のトレイト(=サブモジュール)の生成と破棄を何度も繰り返したいことがある。 - それをやるにはモジュールの中にモジュールをネストさせる必要があるが、これは少しばかり難解だ。
- 外側のトレイト(=モジュール)が自身の
val
による合成
- では、これがどのように見えるか見ていこう。ここに
SystemModule
があり、その中にUserModule
がある。
trait SystemModule { val userModule: UserModule def doStuff() = { userModule.makeUsers() userModule.encourageMemes() // ??? userModule.profit() } }
val
を使うのは、UserModule
のライフサイクルがSystemModule
のそれとは独立しているからだ。SystemModule
にUserModule
を継承させるよりも、このように合成を使った方がいい。そしてval userModule
とSystemModule
の定義は、どこかで互いを Cake にした時に行われる。- 同様に、
UserModule
をパラメータに取る一般的な関数適用とすることもできる。ここではSystemModule
は実際の参照を持たない。
trait SystemModule { def doStuff(userModule: UserModule) = { userModule.makeUsers() userModule.encourageMemes() // ??? userModule.profit() } }
モジュールのネスト化
- 続けて
StorageModule
を見てみよう。StorageModule
はSystemModule
の一部だ。
trait StorageModule { def store(id: Long, data: ByteBuffer) def retrieve(id: Long): ByteBuffer } trait SystemModule extends StorageModule { def doStuff(userModule: UserModule) = { ... } }
StorageModule
はstore
とretrieve
という抽象関数を定義する。ここでは具体的な実装は定義しない。- 問題は、
UserModule
はStorageModule
へアクセスする必要があるということだ。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) = { ... } }
UserModule
はUserModuleModule
になった。酷い名前だが、UserModuleModule
はUserModule
を含んでいる。面白いのは、UserModuleModule
はStorageModule
から派生していることだ。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
だ。bar
はfoo
にアクセスして、それを"World"
という文字列と連結する。しかしfoo
はまだ生成されていない。なぜなら、C
のコンストラクタは自身の親コンストラクタへ委譲している最中で、まだ実行されていないからだ。B
のコンストラクタはC
のコンストラクタへ委譲を返し、ここで問題が起きる。 - これはトレイトを使うと非常によく起きる問題で、そして Cake Pattern は全てがトレイトなので、この問題を頻繁に目にすることになる。一般的に、副作用の有無に関わらず、正格評価 (eager evaluation) は副作用のもう一つの形態だ。これは初期化順序の問題で、ほとんどの場合、コード中の起こるはずがない部分での
NullPointerException
として現れる。理不尽なことが起きたら初期化順序を疑うのは、一般的に良い習慣だ。
予期しない JVM のゼロ
- JVM における予期しない”ゼロ”は、いつでも
null
であるとは限らない。false
かもしれないし、Int
やLong
の場合は0
かもしれない。これに対処しておかないと、問題の切り分けがとても難しくなるし、階層を線形化する方法を把握できなくなることがある。
解決策とパフォーマンス問題
- Precog のコードベース内の Quirrel パーサには、興味深いバグがある。
- これに対する一般的な解決策は、
val
の代わりにdef
かlazy val
を使うことだ。lazy val
は、正格評価を遅延評価 (lazy evaluation) にすることで問題を回避する。 - Scala コンパイラ自身にも興味深い問題がある。scalac のソースコードは、かなり以前から Cake Pattern を使用している。
- scalac は、コンパイラが知る必要のあるポインタである多数のシンボルを持つ。それはクラスオブジェクトの名前であったり、
String
クラスの名前であったり、Integer
やBoolean
のボクシング (boxing) クラスであったりする。 - コンパイラはそれらのシンボルを内部的に知る必要があるので、全てのシンボルを持つ巨大なトレイトが必要になる。問題は、scalac Cake の内部で定義されるシンボルの考え方だ。
- セットアップされた Cake なしでシンボルを生成することはできない。scalac は、文字通り何百もの
lazy val
を持つトレイトを持つ。そして、scalac は特別な関数を使って自身でそれらのlazy val
を正格評価するが、全てのlazy val
を初期化するのに数十秒かかる。とても正気とは思えない!
- scalac は、コンパイラが知る必要のあるポインタである多数のシンボルを持つ。それはクラスオブジェクトの名前であったり、
lazy val
はパフォーマンス問題だ。もし、とにかくパフォーマンスを気にするなら、lazy val
は避けられる限り使ってはいけない。Cake Pattern を使う限りは、この問題を避けるのは難しい。可能ならdef
を使い、もし必要ならlazy val
を使う。foo
を抽象的なdef
にし、bar
をlazy 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.Foo
やmoar.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)
- 問題は
まとめ
val
とlazy val
とdef
のどれを使うのかは、本当に注意する必要がある。一般的には、lazy val
を使って初期化順序の問題を避けるのが正解だ。しかし、lazy val
はロックの順序問題を新たに生み出してしまう。- 僕らが Cake Pattern を使うときは、まず
def
で始めることが多い。つぎに、def
を使うことで奇妙なことが起きたら、代わりにlazy val
を使う。そして、特定のlazy val
でデッドロックが起きることに気づいたら、代わりにval
を使う。さらに、初期化パターンによってはval
がnull
になることがあるので、その場合は泣きながら慎重に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) よりも
extends
やwith
の方が強力で管理しやすい。 - 初期化問題の解決策としてトレイトの代わりに抽象クラスを使う。
- Scala 2.9 と 2.10 のバグについて(レポジトリから scalac の最新版を取ってきて再現するか確かめることもやってるらしい)。
*1:題名の元ネタは”大アマゾンの半魚人 (Creature from the Black Lagoon)”と思われる。ロアナプラはたぶん関係ない、はず。
*2:直前に”val の代わりに lazy val を使え”と言ったばかりである…。
Java 8を関数型っぽく使うためのおまじないをScalaで(ry
はじめに
言うまでもなく下記の記事のパクリです。Java と C# と F# があるのに Scala が無いなんて、と謎の使命感に駆られた結果がこれだよ!
- Java 8を関数型っぽく使うためのおまじない(きしだのはてな)
- Java 8を関数型っぽく使うためのおまじないをC#でやってみた(ぐるぐる〜)
- Java 8を関数型っぽく使うためのおまじないをF#でやってみた(ぐるぐる〜)
まずは、素晴らしい記事を公開して頂いたご両名に感謝を。というか、こういう記事をさらさらと書き下せる方は本当にすごいと思いました。ぼくにはとてもできない。
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") )
もうひとつ関数を定義してみましょう。最初の文字を大文字にする関数です。ちなみに、Scala は JVM 言語なので 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 という言語自体にご興味があればどうぞ。