500 円玉貯金が N 万円貯まったので Thunderbolt Display を購入しました。
現在の開発環境の様子です。
画面が広いと Vim で複数ファイル広げたり、ファイル更新時にはその横でユニットテストが自動実行されたり、別のペインで tig や htop を実行しておくこともできるので便利ですね。
あとは最近少女革命ウテナ DVD-BOX を買ったんですが、これも大画面で観られるようになりました。

そんな大画面でつくった gem を紹介します。

Mr. Mongo とは

yuya-takeyama/mr_mongo

MongoDB の MapReduce の定義を Ruby の内部 DSL で記述し、実行するためのフレームワークです。
Ruby で、といっても map/reduce の関数は JavaScript です。

例えばワードカウントであれば、以下のような記述になります。

map/reduce を __END__ 以降に記述できるのが特徴です。
ここでは使われていませんが、finalize も同様に定義できます。

なお、__END__ 以降の読み込みについては先日開発した InlineTemplateLoader を使用しています。

何で DSL が必要か

ひとつは書き方・定義の仕方を統一するため、もうひとつはユニットテストのためです。

書き方を統一する

MongoDB 標準のやり方にしろ、各言語向けのドライバを使うにしろ、そのままだと人によって書き方がいろいろ異なります。
そこにこの Mr. Mongo を導入することで、「1 ファイル 1 ジョブ」というルールが強制されるようになります。

MapReduce をユニットテストするということ

MongoDB における MapReduce のユニットテストについては、ほとんど言及されることが無いと思いますが、やはり重要なんじゃないかと考えています。
上記のような単純なワードカウント程度の例ならともかく、大量のデータを、複雑に処理し、しかも長期的にメンテナンスを行うのであれば、回帰テストとしてのユニットテストが無くては安心できません。

また、テスト駆動で開発することでより効率よく開発できるケースもあるでしょう。

Mr. Mongo でどうユニットテストを書くか

これについては現状でもできなくはないのですが、フレームワークとしてのサポートがまだ貧弱な状態です。
mr_mongo-rspec-matcher なんてのを作って RSpec でのユニットテストをやりやすくする、というアイディアはあるものの、まだ実装できていません。

一応現在のコードベースでも MapReduce のユニットテストはできています
MapReduce をオンメモリで実行し、その結果を連想配列と比較しています。

これをもっとフレームワークっぽいやり方でできるようになれば、いろいろ捗るんじゃないかと思っています。

あと、以前作った mongo_require.js を併用することで、MapReduce ジョブをある程度モジュール化して記述すれば、これもまたメンテナビリティに寄与すると思われます。

まとめ

MongoDB の MapReduce を効率よく開発・管理するためのフレームワーク Mr. Mongo について紹介しました。

おやすみなさい。

, , , ,

MongoDB で使用する JavaScript 関数をモジュール化して Nodeunit でユニットテストしよう、という話です。

2012 年 12 月現在の Wikipedia によると MongoDB は CommonJS の処理系ということになっているようですが、CommonJS Spec Wiki にその名は見当たりません。
CouchDB は入ってるんですが。

MongoDB を CommonJS 処理系として見たとき、まず一番に辛いのが require 関数が無いことです。
(そもそも require が CommonJS の仕様において必須なのかどうかとかはよく知らない)

require が無くて何が困るかというと、MapReduce に使う関数なんかをモジュール化して書けないので、ユニットテストを書くのが困難、ということです。

そこで、MongoDB において JavaScript コードのモジュール化を促すものを作ってみました。

yuya-takeyama/mongo_require.js

MapReduce に使用するモジュールを作成する

mongo_require.js を使えば、CommonJS と同様のやり方で JavaScript モジュールを作成し、それを MongoDB 内で使用することができます。
それでは定番の WordCount をやってみましょう。

CommonJS のやり方に従って、exports オブジェクトのプロパティとして mapper と reducer の関数をそれぞれセットしています。
ちなみに、ここでは使用していませんが module.exports も使用できます。

これをモジュールとして読み込んで、実際に MapReduce を実行してみましょう。

まず、mongo_require.js を適当な場所に保存します。

そして、MapReduce を実行するのは以下のスクリプト。

はじめに mongo_require.js を読み込むことで、mongo_require() 関数を有効にします。
mongo_require() 関数は CommonJS の require 関数とほぼ同様に使用できます。

ここでの mr オブジェクトにはプロパティとして mapper と reducer それぞれの関数を持っているので、それをそのまま mapReduce 関数に渡しています。

実際に実行すると、以下のように正しく単語数が集計できていることがわかります。
ここではあらかじめ texts コレクションに文が入っていたものとしています。

このように、関数をモジュール化した上で、それを利用しての MapReduce ができました。

モジュールをユニットテストする

関数をモジュール化できたのであれば、それをユニットテストするのも容易になります。

ユニットテストには Node.js など、別の CommonJS 処理系の、既存のテスティングフレームワークを使用することができます。

フレームワークは何でもいいのですが、とりあえずシンプルなもの、ということで今回は Nodeunit を使用してみます。
個人的に Node.js でモジュールを書くときは Jasmine で BDD スタイルに書くのが好きですが、今回のようなシンプルなモジュールであれば、Nodeunit ぐらい素朴なフレームワークの方が向いているのではないでしょうか。

ここでのモジュールの読み込みには普通に require() 関数を使用します。

これを実行すると以下のような結果が得られます。

このユニットテストのポイントとしては以下が上げられます。

  • Mapper 関数内で使用する emit() の大体となる関数をグローバル空間に定義している
  • Mapper 関数をテストするアサーション関数として mapperEmits() 関数を定義している

mapperEmits() 関数は第一引数として Mapper 関数、第二引数として処理するレコードを受け取り、そのレコードを処理したときに emit() されるレコードが第三引数と一致するかをチェックします。
(emit() と mapperEmits() については npm でパッケージ化することを考えています。)

ところで、グローバル関数に依存したテストを書くことは、本来であればアンチパターンとされています。
例えば大規模なプロジェクトにおいてはグローバル関数は極力避けることが望ましいのですが、ここではひとつの MapReduce をひとつの小規模なプロジェクトとして考えています。
プロジェクトの規模が小さければ、グローバル空間汚染によるコンテキストの複雑化もそれほどは問題にならないでしょう。

Reducer 関数については、通常は入力値を元に値を返すだけの参照等価な関数なので、特にこういった特別な関数を用意せずとも deepEquals() 関数でアサーションができます。

まとめ

mongo_require.js を使って MongoDB で使用する関数をモジュール化する方法と、そのモジュールを Nodeunit でユニットテストする方法について紹介しました。

MapReduce は元々 Mapper と Reducer というシンプルな二つの関数の組み合わせで大規模な計算を行う、というアイディアのもとに考えられています。
このシンプルな関数さえ正しく動作することが保証できれば、多少複雑な集計も安心して実装することができますね。ハピラキ。

, , , ,

以前紹介した MapReduce フレームワーク MyMR の開発を少し進めました.
パッケージとしてのリリースはまだですが.

この記事では変更点等について紹介します.

MyMR の仕組みなどについては前回の記事を参照してください.

ビルダーによる Mapper/Reducer の定義

元々はクラスにメソッドを定義する形で行っていた Mapper/Reducer の定義を, ビルダーを使って定義できるようにしました.

この方法を使った場合, Mapper も Reducer も無名関数だけで定義できるようになったので, よりカジュアルになったと言えるでしょう.

また, クラスベースの定義では, 入出力のテーブルをコマンドラインオプションで指定していましたが, こちらではビルダーで指定できるようになりました.
なので設定ファイルの別出しすることもできるようになっています.

また, コマンドラインで指定した場合はそちらが優先されるので, テスト時はテスト用のデータベースを使う, ということもできます.

無名関数以外でも, call_user_func_array() で実行できるもの (PHP 5.4 で言うところの callable) であれば何でもいいので, インスタンスメソッドなどを Mapper として定義することもできます.

簡単なものは無名関数でサクッと定義してい, 複雑なものはユニットテストされたクラスのメソッドで定義する, といった使い分けができるようになっています.

クラスベースの定義も一応残していますが, ビルダーの方が圧倒的に使いやすいように思えるので, そのうち消すかもしれません.

コマンド名の変更

以前は mymr execute コマンドでクラスベースの MapReduce 定義を実行していましたが, ビルダーによる定義の追加に伴い mymr class コマンドに変更しました.

Emitter の切り出し

Mapper はもともと \MyMR \Base クラスの emit メソッドを $this->emit($key, $value) として読んでいましたが, 無名関数で同様のことをやるには PHP 5.4 以降の Closure::bindTo() を使用する必要が出てきてしまうため, Emitter クラスを別に作り, Mapper に引数として渡すようにしました.

これにより, Mapper のテスト時には Emitter をモックオブジェクトや Test Spy オブジェクトに差し替えてテストする, ということもできるようになりました.

進捗の表示

元々は実行しても無言で終了していましたが, Map と Reduce それぞれで進捗を表示するようにしました.

進捗の表示

進捗の表示

今回 \MyMR \Progress という形で実装しましたが, これ自体の MyMR との結合は緩く, Symfony の Console コンポーネントの OutputInterface にしか依存していないので, 別で公開できればと思っています.

まとめ

遊びで作り始めたものですが, 少しずつ様になって来ました.
まだまだ開発が安定しないので, インターフェイス等は大幅に変わる可能性があると思いますが, 引き続き開発を続けて行きたいと思います.

MyMR については, 3/27 の第58回PHP勉強会@東京で発表する予定なので, 後日スライドを公開する予定です. (今からつくる)

あと, MySQL Casual Talks Vol. 3 の LT 枠でも喋りたいと思っているんですけど, これってもう募集していないんでしょうか…

,