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

, , , ,