YAMAGUCHI::weblog

海水パンツとゴーグルで、巨万の富を築きました。カリブの怪物、フリーアルバイター瞳です。

「すごいErlangゆかいに学ぼう! 」という本が出版されました #すごいE本

はじめに

こんにちは、Erlang界のCBR400Rです。このたび、私の2冊めの翻訳書籍、印刷されたものとしては初となる書籍が「すごいErlangゆかいに学ぼう!」というタイトルでオーム社より出版されました。本日より書店ならびにAmazonはじめとするオンラインストアでご購入頂けます。

すごいErlangゆかいに学ぼう!

すごいErlangゆかいに学ぼう!

いま手元にある本の厚さや重さを実際に感じて、電子書籍では味わえなかった充実感、達成感を得ています。これの実現に至るまでに多くの方々にお世話になり、その方々のご協力なしには出版なんて到底ありえませんでした。本当に感謝しています。ありがとうございました。

「すごいErlangゆかいに学ぼう!」はどんな本なのか

これはFred Hebertの書いた"Learn You Some Erlang for great good!"の日本語訳書籍です。著者のFredはこの本の出版を評されて、2012年のErlang User of the Yearに選ばれています。Erlang公式コミュニティからも、推薦の一冊として紹介されている、という書籍です。

Learn You Some Erlang for Great Good!: A Beginner's Guide

Learn You Some Erlang for Great Good!: A Beginner's Guide

タイトルに「Erlang」と入っているので、当然プログラミング言語Erlangに関する本です。ただその内容の量、質が尋常じゃないんです、ほんとに。内容をいくつかの章ごとに大きく分けると、おおよそ次のような構成になっています。

  • 1-9章: Erlangの基礎と関数プログラミングの初歩について
  • 10-13章: 並行プログラミングについて
  • 14-17章: OTPの基礎
  • 18-25章: OTPアプリケーションについて
  • 26-29章: 分散アプリケーションについて
  • 30章と残り: 型と新仕様について

Erlangの本なので1-9章でErlangの文法の習得をするのは当たり前なのですが、それ以降の章はErlangを習得しようとする方のみならず、並行プログラミングや分散アプリケーションを実現する上で一体どのような機構を用意しなければいけないのかを理解したい他言語の方にも十分有用な内容となっています。OTPという「フレームワーク」がいかに先に述べたようなアプリケーションを容易にかつ堅牢に実現しているかを理解する上で、本書の構成が非常に優れていると私個人は思っています。その理由はこの本がOTPありきで進まないからです。まずかならず手持ちの機能だけで頑張ってアプリケーションを実装してみて、いよいよ実装が複雑になりお手上げとなったところでOTPを紹介し、その有用性を実感できる形になっています。一つ一つ確実にその有用性を理解することで、Erlangに限らず他の言語でも同様のアプリケーションを実装する際には似たような機構が必要になることを直感的に理解できるのではないでしょうか。このような内容を自分で手を動かしながら理解できる日本語書籍は、自分で言うのもなんですが、他に知りません。

また理解が進めば進むほど、OTPの強力さを思い知らされることになります。普通の言語では標準ライブラリ等はあくまで言語に付随するもので、とりだてて別称することはありません。しかしErlangではわざわざErlang/OTPと標準ライブラリであるOTPを併記します。個人的にはこの辺りにOTPの重要性が垣間見えると思っています。つまりOTPというフレームワークがあるからこそ、Erlangが存在しうる、という意志の表れではないでしょうか。

「新しい言語を学ぶ」というだけではなく「新しい仕組みを知る」という点でもおすすめです。ぜひご一読下さい。また本書は原著版にもない、最新のR17.0から導入されたマップについてのまるまる1章追加されています。R17.0対応している日本語のErlang書籍は本書だけです!

翻訳の思い出

この翻訳書籍を出版するきっかけは3年半前までさかのぼります。*1 訳者序文にもありますが、まだ僕が転職する前に、レビュアーの @voluntas から原著のウェブ版(当時はまだウェブ版しかなかった)を紹介されたのがきっかけでした。そして、Erlangという、よくわからないけどなにやら並行・分散プログラミングが非常に得意でネットワークサーバの作成にすこぶる向いている面白い言語がある、と前々から聞いていたので、勉強がてら翻訳を開始しました。

始めのほうは順調に翻訳を行っていたものの、後半になるにつれどんどん1章ごとの分量が増え、さらに内容も複雑化して、非常に苦労していたことを覚えています。その後、途中で別のことをしていたり、仕事が忙しくなったりして翻訳が滞り、ウェブ版の原著が30章まで完成しいよいよ印刷版が出るという噂が出始めた頃には、まだ22章までしか翻訳が終わっていませんでした。その後しばらくして転職をし、そろそろ翻訳を再開しようかと思っていた頃に、オーム社の鹿野さんと高尾さんに今回の翻訳書籍の出版のお話を頂きました。

とりあえずウェブ版原著の翻訳を終えることが肝心ということで、なんとかウェブ版原著の翻訳を終えました。その後、原著の書籍版が出版され、オーム社さんにウェブ版と書籍版の突き合わせ原稿を作成してらっているうちに、またバタバタしだしてしまい、翻訳が滞ってしまい、結局今年の頭から書籍版の翻訳を再開。とまあ、書籍版自体の翻訳を開始するまでがすでにウェブ版の翻訳から3年経っているという状況だったのですが、書籍版の翻訳が始まってからは、多くのレビュアーの方に的確なレビューをいただき、短い期間で非常に多くの改善を行うことが出来ました。現在公開されているウェブ版の日本語訳と比較すると、恥ずかしくてウェブ版を取り下げたいレベルです。

自分の中で4年間弱常に頭の片隅にあったモヤモヤが、こうやって無事に出版にこぎつけた今日ようやく消えた気がします。あー、長かった。

オーム社での翻訳体制の話

ウェブ版の翻訳は個人のBitbucketのhgレポジトリ、後半になってGitHubのprivateレポジトリ、そして書籍版の翻訳は最初はsvn+tracを使い、2013年11月からはオーム社GitHubのプライベートレポジトリに移行しました。このように何度かレポジトリの引越しをして出版まで来たわけですが、最後のGitHubでの運用が一番個人的に楽でした。運用方法は特に明文化されていたわけでなく、レビュアーに自由にチケットを切っていただきながら、ラベルやマイルストーンを適宜追加していったわけですが、いま振り返ると次のような方法で翻訳するのが楽だったなと思いました。

編集原稿

鹿野さんが用意して下さったのですが、拡張HTMLのようなマークアップ原稿でした。基本HTMLなのですが、原稿には日本語と英語原文の両方があり、日本語の部分のみ編集。たとえばつぎのような感じです。

<p lang="en">Learn You Some Erlang for great good!</p>
<p lang="ja">すごいErlangゆかいに学ぼう!</p>

このlang="ja"の部分だけど編集する形。GitHubへpushするとCIが回され、そこでLaTeX化の後にPDFになり、毎日夜中に最終版のPDFが共有のWebDAVサーバに置かれる形でした。

この原稿がよかったのは、CSSがきちんと用意されていたので、ローカルでもブラウザで簡単に読めたこと、そしてHTML、PDFという異なる形式でレビューできたので間違いを拾いやすかったことです。逆になれないと大変だったのは、HTMLを直接さわる感じなので、最初はタグがチラチラ目障りに感じたこと、そして < などの特殊記号のエスケープがたまに必要だったことぐらいです。自分の好きなエディタで気軽に編集できたのは助かりました。

ブランチ戦略

基本masterブランチ1本で、Pull Requestがある場合にのみブランチを切ってもらいます。ソースコードほど厳密にfeatureがあるわけでもないので、ブランチ名はレビュアー名とか、適当に分かる名前をつけてもらいました。mergeに関しては翻訳者である自分と編集者の鹿野さんが行うのみ。自然言語のmergeは時折うまくいかないことがあり、mergeツールに騙されたことが何度かありましたが、ほぼ問題なく運用できていました。

レビューのissueの切り方

それぞれにまちまちで、それぞれにわかりやすい点はあったのですが、個人的に一番対応しやすかったのは次のようなissue。

  • タイトル: <章番号> <該当箇所 or issue概要>
  • 本文: <GitHub上の該当箇所への行リンク> <issue内容>

GitHubでは行へのリンクが可能なので、ブラウザ上で読むときにリンクが該当箇所のリンクがあるのは非常に便利。さらに、行リンク内に行番号があるので、実際に手元で編集を行う上でも行リンクは便利でした。編集後に行番号がずれることはありましたが、基本的に10行もずれないのでこの方法でうまく行きました。

編集コメントの扱い

最初は僕と編集の方だけでやりとしていたので特にissueとか上げず タグでやりとしをしていました。が、レビューの段になって、編集コメントで質問されている内容が後々まで放置されてしまい、最後で慌てて回収するという事がありました。組版や作業報告以外は編集コメントではなく、issueに上げたほうが皆の目が入るし幸せかなと思いました。(もしくは両方に記載する)

おわりに

訳者序文でも書いていますが、本書の出版はレビュアーの方々のご協力なしには到底ありえませんでした。私がErlangを業務で書いているわけではない中で、実際にErlangで、しかも先端で仕事をされている方々からの実務経験に基づいた細かなアドバイスや、他の関数型言語のエキスパートとしての側面からの指摘、さらには他言語の経験がありつつErlang初学者としての視点からのフィードバッグなど、果ては私の拙い日本語の修正など、多くのレビューを頂きました。この場を借りてレビュアーの皆様に改めて感謝いたします。

書籍版レビュアーのみなさま(五十音順)

  • @ajiyoshi さん
  • 幾田雅仁さん (@cooldaemon)
  • 上西康太さん (@kuenishi)
  • 篠原俊一さん (@itawasa)
  • 渋川よしきさん (@shibu)
  • 島崎清山さん (@seizans)
  • 中居良介さん (@voluntas)
  • 廣江 深さん (@hiroe_orz17)
  • 山本和彦さん (@kazu_yamamoto)
  • 力武健次さん (@jj1bdx)
  • 若山史郎さん (@r_rudi)

Special Thanks

  • 古瀬 淳さん (@camloeba) :型について

オーム社

  • 鹿野桂一郎さん (@golden_lucky)
  • 高尾智絵さん

皆様からのレビュー(随時追加)

*1:原著者のFredに翻訳の許可をもらうメールを送ったのが2010年12月22日でした。

(翻訳) Erlang Webライブラリ&フレームワーク

はじめに

こんにちは、Python界の情弱です。まだWSGIに関する諸々のエントリを訳したいのですが、その前にErlangで気になっていたWeb系のライブラリの比較記事があったので見てみます。僕の感想とは違うところもありますし、みなさんの感想とも違うかもしれませんが、そういう場合はぜひ本人にメールしてあげてください。
http://lenary.co.uk/erlang/2011/08/erlang-web-libraries/
原文には各ライブラリへのリンクが無かったので付け足しておきました。

Erlang Web Libraries & Frameworks

最近、簡単な社内APIのエンドポイントを書いている友人のために、必要に駆られて幾つかのErlang Webライブラリとフレームワークを比較しました。彼が是非多くの人にその要約を公開するべきだと言ったので、ここに公開します。
元記事を公開してから訂正や更新を教えてくださった皆様に感謝します。
(この記事は、より多くの情報を正確にしておきたいと思います。何か違っていることがあれば是非Emailをください。)

Yaws

Yawsは動的なWebページのサーバを想定した高性能Webサーバです。しかしながら、appmodsを使うことによって、代わりにAPIを作ることも可能です。Yawsのドキュメントはよく書かれていて、とても良くまとまっているようです。監視ツリーの中に入れることも可能です。
(Steve VinoskiさんにYawsが抜けていることを訂正してもらいました。ありがとうございます。)

Mochiweb

ErlangAPIの開発をする時に一番使われているライブラリのようです。全体としてよく機能していますが、ほんの少し気まぐれな動きをしたりします。Mochiwebのドキュメントはあまり良くありませんし、パラメータ化されたモジュールを多用します。個人的にはあまり好きな方法ではありません。とは言うものの、他のErlangアプリケーションに組み込む上では些細なことです。

Misultin

Misultinは一般的なアーキテクチャはMochiwebにとても良く似ていますが、WebSocketのサポートも含む点が違います。サンプルはとてもよくまとまっていて、簡潔です。Wikiに多くのドキュメントもあります。
(Andrew Thompsonさんに指摘して頂き、このセクションの他にCowboyとZotonicのセクションも追加しました)

Cowboy

CowboyもMochiwebにとてもよく似ています。リクエストを捌くのに、パラメータ化されたモジュールの代わりに普通にモジュールを使います。(私が思うに)Cowboyは、速度の向上とメモリ消費量削減のために、すべてバイナリで処理する唯一のHTTP Serverです。githubのレポジトリに使い方の例があります。

httpd

httpdモジュールはinetsアプリケーションの一部としてバンドルされています。多くのユーザにはこれで十分で、また素早くコードを書きたい場合にも有用です。httpdWSGIやRackのようなミドルウェア、つまり任意のリクエストハンドラをつなげるミドルウェアをサポートする唯一のモジュールです。
残念なのは、単一のコールバック関数を使うことに固執していること、ドキュメントがあまり優れていないことです。httpdモジュールを使って何か作るときには、Garrett Smithが書いたmodlibというラッパライブラリを使うとAPIがいくらかはすっきりします。

SimpleBridge

ラッパーといえば、SimpleBridgeは「Erlang HTTPサーバに単純で、標準化されたインターフェースを提供する」というプロジェクトです。これが実際に意味するところは、使っているフレームワークのリクエスト/レスポンスオブジェクトをSimpleBridgeアダプタにラップして、実際に使っているライブラリを取り替え安くする」ということです。
私は、SimpleBridgeはHTTPライブラリアダプタとして十分機能するにはまだまだだと感じます。SimpleBridgeはMochiweb, inets httpd, YawsやMisultinのパイプライン内のサポートもしています。
(Lo〓c HoguinとJesse Gummの両者にSimpleBridgeを指摘してくださったことを感謝します)

webmachine

これはMochiweb上に構築されていますが、多くの低レベルHTTPセマンティクスから解放してくれます。代わりに"RESTツールキット"を使うことになります。基本的なRPC over HTTPアプリケーションには、このモジュールを使うことはないでしょう。しかしながら、もしアプリケーションがリソース指向であれば、これはとても役に立ちます。Bashoから出てきたモジュールなので、よいコードが書かれていてドキュメントも良いものであることが期待されます。

Nitrogen

Nitrogenは関数型的なユーザインタフェースを作ることにより重点を置いています。これによって、クライアントとサーバの間でイベントを送り合えるようになりました。これはとても興味がそそられます。ここにNitrogenの紹介があります。各ページはただのモジュールで、テンプレートはオプションです。NitrogenでAPIを作るのは簡単です。
(Jesse GummがNitrogenでAPIエンドポイントを作ることができると教えてくれました。ありがとうございます。)

Zotonic

Zotonicは本格的なCMSで、WebSocketやCometのサポートもしています。拡張も簡単で、非常に多くのドキュメントが彼らのサイトに載っています。私からお伝えできることとしては、webmachineとNitrogenを使っているけれど、サイト内にOAuthサポートもできる独自のAPIを定義することも可能です。

Chicago Boss

このフレームワークRubyにとってのRailsのようなものです。モデルと、多くの実装済みビヘイビアが提供され、いち早く開発が出来るようになっています。おそらく、単純なAPIには複雑すぎますが、あなたが何をするかによってそれも変わるでしょう。

なぜErlangなのか

はじめに

最近はErlang関連のWeb書籍の翻訳をしてまして*1Erlang関連のニュースなどを収集して読んでいます。そこでたまたま見つけた面白そうなエントリがあって、翻訳しようかなと思っていたら@voluntasの兄貴ご推薦ということなので、日常生活を取り戻すべく翻訳してみました。
このエントリはinagistというサービスの公式ブログで、ErlangWebサービスを作ることの利点を説明したエントリです。多少補足や外部リンクが必要な部分は僕がリンクを貼ったりしているので、わからない場合はご参照ください。あと翻訳が怪しいところがあるのでツッコミ歓迎。

なぜErlangなのか

よく私がinagistはErlangで書かれていると言うとおかしな目で見られることがあります。なので、ここでErlangが適している重要な点をいくつかあげようと思います。

私たちがしていること

inagistでは、リアルタイムでツイートストリームを要約しようとしています。今のところはTwitter Streaming APIを使って動作させています。要約することによって、リアルタイムにストリーム中で人気があるツイートをフィルタリングして、トレンドに応じてツイートをグループ分けしています。これを実際にjustinbeiber.inagist.comlibya.inagist.comで見てみましょう。(WebSocketを使っているのでGoogle ChromeSafariが最適です。)こういった要約を、いくつもの方法で組み合わせたストリームに対して行っています。ユーザ独自のストリーム(マイストリーム)、キーワード検索ストリーム(libya.inagist.com)、キーワード+ジオロケーションベースストリーム(sxsw.inaginst.com)がそれです。

軽量プロセス

ここではっきり意識しておかなければいけないのは、ツイートストリームを要約するときのリアルタイム性です。ストリーム中の個々のツイートを可能なかぎり永続化して、オフライン分析をするという方法もあります。しかしここでは代わりにユーザ毎に有限なキャッシュを作って、あるツイートが人気がでたり、トレンドを検知するときにストリーム中でキーワードが繰り返されたら、キャッシュに押し込んだり取り出したりしたりし続けています。これがErlangに適している部分なのです。ストリームのコンシューマはErlangプロセスとしてモデル化されています。理由はErlangプロセスが軽量で、独立しているからです。本質的にはユーザ毎に用意されたプロキシです。このErlangプロセスはストリームからツイートを受け取って、キャッシュを操作して、データを返すためにAPIクエリに応答します。これらのストリームコンシューマはそれぞれgen_serverによる実装になっていて、supervisorチェインに結びついています。コンシューマが落ちたときには、supervisorが他のユーザに全く影響を与えずに再起動します。

メッセージ

では、どうやって取得したデータストリームとこの軽量プロセスを、メッセージを使って結びつけるのでしょうか。個々のツイートはコンシューマにStreaming APIクライアントからのメッセージとして届けられます。ツイートのコンシューマはそれぞれErlang VM上全範囲に渡るプロセスツリーの一部となっています。ツイートがネットワークから取得された瞬間に、ツイートが別のプロセスとして起動されて、JSON化されたツイートを処理して、分散されたプロセスツリーにメッセージを投げます。メッセージはコンシューマプロセスに渡されて、コンシューマプロセスがキャッシュの更新を行います。非同期メッセージなので、クライアントではツイート一つに対してどれだけコンシューマがあるかは関係ありません。コンシューマプロセスがあるツイートに関連があるなら、ただ処理できるのです。メッセージは分散して送出されているので、取得するツイートの負荷が大きくなったときやコンシューマの数が増えたときにはスケールします。

分散Erlang

コンシューマの数が増えるにつれて、Erlangの他の重要な側面が役に立ってきます。マシン間での分散です。コンシューマプロセスは設計においてはプロセスIDでしか認識できないようになっています。ErlangではローカルプロセスIDと分散プロセスIDを同様に扱います。コンシューマが分散ツリーの一部であるかぎり、ツイートはプロセスに届けられます。これはスケールアウトを容易にします。個々のマシンはクラスター全体に影響を与えることなく独立して落ちることができ、ただそのマシンが落ちている間はそのマシン上に配置されたユーザがオフラインになるだけになります。

リアルタイム配布

出来るだけリアルタイムにするために、接続されたクライアントにはWebSocketを使ってメッセージを送ります。ここでメッセージが再び登場です。ストリームコンシューマはそれぞれメッセージを生成して、キャッシュが人気のあるツイートやトレンドを明らかにします。我々のアプリのGoogle ChromeプラグインはWebSocketクライアントで、この配布モデルを使っています。Chrome拡張はトレンドがわかったり、ツイートにある程度以上人気が出たら通知をします。また拡張を使って別の角度からもリアルタイム検索をしています。トレンドがわかったときに、拡張が自動的にそのトレンドに沿った目立ったツイートを探し始めます。

ストリーミング検索

以前に検索ストレージとしてどのようにRiakを使っているかに触れました。それに加えて、ストリーミング検索を可能にする独自の拡張を行いました。Riakでドキュメントにインデクスを張るときに常にインデクスデータを検索基盤に送ります。またこのインデクス項を、ツイートが別のインデクスに取得されたらときにもすぐに送ります。ここでシステム上には、パターンマッチをするために のタプル待っているプロセスや、検索基準に合致するドキュメントのプロセスを待っている通知があります。いまのところ ==, and, or, >, <, >=, =< といった演算子をサポートしていて、sxsw(==)、justin beiber(and)、文字列を含んだドキュメント、バウンディングボックス内の緯度経度など、どんなツイートでも検知できます。ストリームコンシューマは、これを使ってストリームからリアルタイムにフィルタされたツイートを取得します。Chromeプラグインでもこの検索ストリームをつついて、検出されたトレンドや検索クエリにあったのツイートがあったときにはいつでも、ユーザに通知をします。この機能は本当に強力です。なぜなら、これがあればトレンドを見ることでユーザがどんな話題に興味があるかとか自動的に知ることができ、ユーザにリアルタイムにこういった話題に合致するものがあったときにいつでも知らせることができます。このストリーム検索は、メッセージベースのアーキテクチャの性質によって、小さいオーバーヘッドで動作しています。この性質によってブラウザで1秒か2秒くらいでストリームを処理することができるのです。justinbieber.inagist.comなどのページのヘッダにある"Live Stream"をクリックすれば、実際に動作しているところを見ることが出来ます。

おわりに

以上、Erlangの差別化要因に関して俯瞰してお話しましたが、わざわざErlangを使っている理由はご理解いただけかと思います。もしもっと情報が必要ということであれば @jebui までコメントをください。喜んでお返事致します。

追伸: さっきツイートストリームで話題になっていたJustin BieberかLady Gagaのアルバムはまだ聞いてません。

訳者感想

当たり前な話ですがErlangは誰でも簡単にスケールするアプリケーションが書けるわけではなく、やはり書くためにはそれなりに経験が必要だと思いますが、言語/標準ライブラリ自体がそれを意識した作りになっているので、きちんと勘どころを抑えれば安全に早くそういったものが書ける土台があるという点で素晴らしいですね。これから始める方に、ご紹介。

    • 拙訳のErlangのWeb書籍とりあえず始めてみる方にはおすすめです

そしてちゃんとやるんだったら戦闘機本がおすすめですよ。

プログラミングErlang

プログラミングErlang

*1:遅筆ですみません

自前ビルドでerl -manを動くようにする

はじめに

Erlangのドキュメントってすごく良く書いてあるんだけど、HTMLの物って検索がブラウザだのみだったりして好かないのでコマンドラインでそのまま見れる"erl -man"コマンドで見ようとおもったらこんなエラーがでやがった。

$ erl -man lists
No manual entry for lists

これの直し方が分からなくてちょっと手間取った。

作業ログ

ソースからドキュメントをビルドする

よくよく調べてみたらここに記述があった。ここの3.6参照。

これにしたがってソースディレクトリに行ってビルド。

$ cd /opt/erlang/src/otp_src_R14B
$ make docs
$ make install-docs
$ erl -man file
file(3)                           Erlang Module Definition                                    file(3)

NAME
       file - File Interface Module

DESCRIPTION
       The module file provides an interface to the file system.
...

できた。

ミスった作業

format_man_pagesを使う

ググってたら昔のバージョンではformat_man_pagesでやりました、的な記載を見つけたのでやってみた。

$ mkdir $ERLANG_HOME/man
$ cd $ERLANG_HOME/lib/erlang/misc
$ ./format_man_pages $ERLANG_HOME
Formatting manual pages (this may take a while...)

あれ?終わったのか?あまりにも一瞬でできたので怪しい。中身を見てみると...

$ ls $ERLANG_HOME/man/*
/opt/erlang/R14B/man/cat1:

/opt/erlang/R14B/man/cat3:

/opt/erlang/R14B/man/cat4:

/opt/erlang/R14B/man/cat6:

全部空じゃねえかよ。

manファイルをダウンロードして入れる

ここに作成済みのmanファイルがあるので落としてきて解凍。今回はR14Bなので

$ curl -O http://www.erlang.org/download/otp_doc_man_R14B.tar.gz
$ tar xzf otp_doc_man_R14B.tar.gz
$ mv ./otp_doc_man_R14B/man $ERLANG_HOME
$ export MANPATH=$ERLANG_HOME/man:$MANPATH
$ erl -man lists
No manual entry for lists

あれおかしいな?

EUnitとOMakeでテスト駆動開発

はじめに

こんにちは、セコムしてますか?僕はしてません。さて、Erlangでちょこちょこと質問してたら、id:kuenishiから「てめえ、ぐだぐだ言う前にまずOMakeとEUnit使ってひたすらテストしろや」と言われたのでそのセットアップなどをしました。

参照

書いたもの

とりあえずテンプレートとして使えるように置いておきますね。更新出来るところから更新します。

設定

まずはコードを書く

テスト以前にそもそものコードを書きますよね。というわけで今回は「決められた値までの素数列を求める関数primesを実装する」ということで書いてみました。

bottom(List)->
    case List of
        [] -> [];
        [X] -> X;
        [_|Xs] -> bottom(Xs)
    end.


sift(Divider, Nums)->
    lists:filter(fun(X) -> X rem Divider =/= 0 end, Nums).


sieve(Nums)->
    sieve([], Nums).
sieve(Accu, Nums)->
    case {Accu, Nums} of
        {_, []} ->
            Accu;
        {[], _} ->
            sieve([hd(Nums)], sift(hd(Nums), tl(Nums)));
        {_, [X|Xs]} ->
            B = bottom(Xs),
            if 
               X*X > B ->
                    lists:reverse(Accu, Nums);
               true ->
                    sieve([X|Accu], sift(X, Xs))
            end
    end.

primes(Limit)->
    sieve(lists:seq(2, Limit)).
テストコードを書く

さて、ちゃんと上のコードが動くのかっつーわけで、「並行して」テストコードを書いていきますよ。並行して、っていうか関数名書いたら実装始める前にテスト書きますよね。期待する結果を書いて。でこんな感じでテスト書きました。

bottom_test_()->
    [?_assert( [] =:= bottom([]) ),
     ?_assert( 1 =:= bottom([1]) ),
     ?_assert( 10 =:= bottom(lists:seq(1, 10)) ),
     ?_assert( d =:= bottom([a, b, c, d]) )
    ].

sift_test_()->
    [?_assert( [] =:= sift(2, []) ),
     ?_assert( [] =:= sift(1, lists:seq(1, 5)) ),
     ?_assert( lists:seq(1, 9, 2) =:= sift(2, lists:seq(1, 10)) )
    ].

sieve_test_()->
    [?_assert( [] =:= sieve([]) ),
     ?_assert( [1] =:= sieve(lists:seq(1,10)) ),
     ?_assert( [2,3,5,7] =:= sieve(lists:seq(2,10)) )
    ].

primes_test_()->
    [?_assert( [2] =:= primes(2) ),
     ?_assert( [2,3,5,7] =:= primes(10) )
    ].

_test_()で終わる関数を書いておくと、EUnitはそれをテストジェネレータとして認識して全部実行してくれます。すごいですねー。

コンパイルと実行

さて、コード書いただけではクソの役にも立たないのでコンパイルして実行します。上の2つを全部まとめたのがこれ。

%%%-----------------------------------------------------------------------------
%%% @author Yoshifumi YAMAGUCHI <ymotongpoo AT gmail.com>
%%% @copyright (C) 2010, Yoshifumi YAMAGUCHI
%%% @doc
%%%
%%% @end
%%% Created : 23 Oct 2010 by Yoshifumi YAMAGUCHI <ymotongpoo AT gmail.com>
%%%-----------------------------------------------------------------------------

-module(sample).
-define(EUNIT, true)
-export([bottom/1, sift/2, sieve/1, primes/1]).
-compile(export_all).

%%%-----------------------------------------------------------------------------

bottom(List)->
    case List of
        [] -> [];
        [X] -> X;
        [_|Xs] -> bottom(Xs)
    end.


sift(Divider, Nums)->
    lists:filter(fun(X) -> X rem Divider =/= 0 end, Nums).


sieve(Nums)->
    sieve([], Nums).
sieve(Accu, Nums)->
    case {Accu, Nums} of
        {_, []} ->
            Accu;
        {[], _} ->
            sieve([hd(Nums)], sift(hd(Nums), tl(Nums)));
        {_, [X|Xs]} ->
            B = bottom(Xs),
            if 
               X*X > B ->
                    lists:reverse(Accu, Nums);
               true ->
                    sieve([X|Accu], sift(X, Xs))
            end
    end.

primes(Limit)->
    sieve(lists:seq(2, Limit)).


%%%-----------------------------------------------------------------------------

-ifdef(EUNIT).
-include_lib("eunit/include/eunit.hrl").

bottom_test_()->
    [?_assert( [] =:= bottom([]) ),
     ?_assert( 1 =:= bottom([1]) ),
     ?_assert( 10 =:= bottom(lists:seq(1, 10)) ),
     ?_assert( d =:= bottom([a, b, c, d]) )
    ].

sift_test_()->
    [?_assert( [] =:= sift(2, []) ),
     ?_assert( [] =:= sift(1, lists:seq(1, 5)) ),
     ?_assert( lists:seq(1, 9, 2) =:= sift(2, lists:seq(1, 10)) )
    ].

sieve_test_()->
    [?_assert( [] =:= sieve([]) ),
     ?_assert( [1] =:= sieve(lists:seq(1,10)) ),
     ?_assert( [2,3,5,7] =:= sieve(lists:seq(2,10)) )
    ].

primes_test_()->
    [?_assert( [2] =:= primes(2) ),
     ?_assert( [2,3,5,7] =:= primes(10) )
    ].

-endif.

早速コンパイルして実行ですね。EUnitはmというモジュールだったらm:test()という関数として組み込まれるので、これを実行してあげればよいわけです。

$ erlc sample.erl
$ erl -noshell -s sample test -s init stop
  All 12 tests passed.

おお、ちゃんとテスト通りましたね。じゃあわざとこけるようにテストを書き換えてみます。

bottom_test_()->
    [?_assert( [] =:= bottom([]) ),
     ?_assert( 1 =:= bottom([1]) ),
     ?_assert( 1 =:= bottom(lists:seq(1, 10)) ), % ここを10じゃなくて1にしてみました
     ?_assert( d =:= bottom([a, b, c, d]) )
    ].

再度コンパイルして実行。

$ erlc sample.erl 
$ erl -noshell -s sample test -s init stop
sample:59: bottom_test_...*failed*
::error:{assertion_failed,[{module,sample},
                         {line,59},
                         {expression,"1 =:= bottom ( lists : seq ( 1 , 10 ) )"},
                         {expected,true},
                         {value,false}]}
  in function sample:'-bottom_test_/0-fun-4-'/0

=======================================================
  Failed: 1.  Skipped: 0.  Passed: 11.

ちゃんと?こけてくれてますね。他の11個のテストに関しては通りましたよ、って出てます。ここまでで手動でEUnitを走らせるところまでは来ました。あとはOMakeを使って、ファイルを更新するたびにコンパイルとEUnitの実行をしてもらえるようにすればいいだけです!

OMakefileの設定

OMakeはOCamlで実装されてるビルドツールで、OMake言語という専用言語でGNU Makeよりも柔軟な記述が出来ます。さらにomake -Pというオプションを点けて実行すると、Flymakeのようにファイルを更新するたびにビルドが走ります。早速OMakeを書いてみます。詳しいことは上記リンクに任せるとして、今回は次のようなディレクトリ構成にしました。

unittest/
├── OMakefile
├── OMakeroot
└── src
    ├── OMakefile
    └── sample.erl

OMakerootはプロジェクトのルートディレクトリに必ずないといけないファイルで、これを起点としてディレクトリを行ったり来たりできます。OMakefileはプロジェクト内の各ディレクトリに置いておくファイルで、この中でビルドとか書いたりします。
OMakerootは完全にid:kuenishiのやつをパクリましたwのでOMakefileで使ってない設定がたくさんあります。いずれ使います。

  • OMakeroot
########################################################################
# The standard OMakeroot file.
# You will not normally need to modify this file.
# By default, your changes should be placed in the
# OMakefile in this directory.
#
# If you decide to modify this file, note that it uses exactly
# the same syntax as the OMakefile.
#

#
# Include the standard installed configuration files.
# Any of these can be deleted if you are not using them,
# but you probably want to keep the Common file.
#
open build/C
open build/OCaml
open build/LaTeX

# sth like OTP.om
public.ERLC=$(shell which erlc)
public.SRCDIR=src
public.TESTDIR=test
public.EBIN=ebin
public.ERLCFLAGS=-DDEBUG +debug_info -Wall
public.ERL=erl
public.ROOT=$(shell pwd)

public.BEAM_EXT=.beam
public.ERL_EXT=.erl

public.INCLUDES[] = include
public.INCLUDES_OPT = -I
#
# Add the -I option to the includes lazily.
# Don't redefine this variable unless you know what you are doing.
#
public.PREFIXED_INCLUDES = $`(addprefix $(INCLUDES_OPT), $(INCLUDES))

#%$(BEAM_EXT): %$(ERL_EXT) #:scanner: scan-erl-%$(ERL_EXT)
#    $(ERLC) $(ERLCFLAGS) $(PREFIXED_INCLUDES) $<

MakeBeams(names) =
    erl2beam(name) =
        private.beam = $(EBIN)/$(removesuffix $(basename $(name)))$(BEAM_EXT)
        $(beam): $(name)
            $(ERLC) $(ERLCFLAGS) -pa $(EBIN) $(PREFIXED_INCLUDES) -o $(EBIN) $<
#            $(ERL) -pa $(EBIN) -noshell -eval '$(removesuffix $(basename $(name))):test().' -s init stop 
        return $(string $(beam))
    private.beams=$(names.map $(erl2beam))
    return $(beams)

erls=$(glob $(string $(SRCDIR)/*$(ERL_EXT)))
beams=$(MakeBeams $(erls))
apps= #$(glob $(string $(EBIN)/*.app))

#
# The command-line variables are defined *after* the
# standard configuration has been loaded.
#
DefineCommandVars()

#
# Include the OMakefile in this directory.
#
.SUBDIRS: .
  • OMakefile
########################################################################
# Phony targets are scoped, so you probably want to declare them first.
#
.PHONY: all clean

# eprintln( $(string $(beams)) )
testerls=$(glob $(string $(TESTDIR)/*$(ERL_EXT)))

# traverse the subdirs except $(dirs)
Subdirs_except(dirs) =
 # need to export since .SUBDIRS is evaluated in the global scope
 export VISIT_SUBDIRS

 sub_omakefiles = $(glob i, */OMakefile)
 subdirs = $(sub_omakefiles.map $(dirname))

 VISIT_SUBDIRS=$(set-diff $(subdirs), $(dirs))

 # The rule
 .SUBDIRS: $(VISIT_SUBDIRS)

# traverse all the subdirs
Subdirs() =
 Subdirs_except($(array))

Subdirs()
  • src/OMakefile
MODULE = sample
FILES = $(MODULE).erl
TARGET = $(MODULE).beam

.PHONY: all $(TARGET)

.DEFAULT: all

all: $(TARGET)

$(TARGET): $(FILES)
    $(ERLC) $(FILES)
    $(ERL) -noshell -s $(MODULE) test -s init stop

clean:
    rm -f *.beam

これで準備完了。プロジェクトルートでomakeを打ってもいいですし、srcディレクトリ内で打ってもいいです。

$ omake
*** omake: changing directory to /Users/ymotongpoo/src/erlang/unittest
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.02 sec)
- build src <.DEFAULT>
+ erl -noshell -s sample test -s init stop
  All 12 tests passed.
*** omake: done (1.43 sec, 0/0 scans, 1/1 rules, 0/73 digests)      

ちゃんとEUnitが走りました。

omake -Pで幸せ

さていよいよomake -Pで幸せになりたいと思います。今回はtakeN/2というリストの先頭からN個の要素を取ってきたリストを返す関数を追加したいと思います。まずコマンドラインでおもむろにomake -Pを起動させます。

$ omake -P
*** omake: changing directory to /Users/ymotongpoo/src/erlang/omake-eunit-template
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.02 sec)
- build src <sample>
+ erl -noshell -s sample test -s init stop
  All 12 tests passed.
*** omake: done (1.42 sec, 0/0 scans, 1/1 rules, 0/86 digests)                                                             
*** omake: polling for filesystem changes

ここで通常のomakeと違い、最後に一行、ポーリングして監視してますよ、って感じのメッセージが出てると思います。ここでsample.erlに以下のような行を追加して保存します。

takeN(N, List)-> % わざと関数名だけ書いて放置します
...
takeN_test_()-> % テストを書いておきます
    [?_assert( [] =:= takeN(0, lists:seq(1,10)) ),
     ?_assert( [1] =:= takeN(1, lists:seq(1,10)) ),
     ?_assert( lists:seq(1,5) =:= takeN(5, lists:seq(1,10)) ),
     ?_assert( lists:seq(1,10) =:= takeN(20, lists:seq(1,10)) )
    ].

するとomake -Pを立ち上げているターミナルを見るとこんなメッセージが追記で表示されているのがわかるでしょう。ちゃんと保存したらomakeが走ってますね。

*** omake: file src/sample.erl changed
*** omake: rebuilding
- build src <sample>                                                                                                       
+ /opt/erlang/R14B/bin/erlc sample.erl
./sample.erl:58: syntax error before: '.'
./sample.erl:93: unbalanced '-endif'=========================================================              ] 00021 / 00024
./sample.erl:86: function takeN/2 undefined
./sample.erl:87: function takeN/2 undefined
./sample.erl:88: function takeN/2 undefined
./sample.erl:89: function takeN/2 undefined

こんな感じでずーっと編集していくと、ターミナルを眺めながらコードを書いてればよい状態になって幸せです。(Emacsとかならshell-modeで立ち上げていると完全にEmacsの中だけで完結します)
テストだけ先に書いてるのであとは保存したときに下記のように「テスト通りました!」って状況になるまでコード書いてればいいだけ!幸せ!

...
      ./sample.erl:92: unbalanced '-endif'
      ./sample.erl:51: function takeN/3 undefined
*** omake: polling for filesystem changes
*** omake: file src/sample.erl changed
*** omake: rebuilding
- build src <sample>                                                                                                       
+ erl -noshell -s sample test -s init stop
  All 16 tests passed.
*** omake: done (10 min 26.08 sec, 0/0 scans, 6/6 rules, 10/158 digests)                                                   
*** omake: polling for filesystem changes

追記

eunit.hrlへのパス

上記ドキュメントではEUnitを使う際は

-include_lib("eunit/include/eunit.hrl").

を追加しなさいよ。そしてビルドするときは次のようにeunit.hrlを含むeunitという名前のディレクトリパスを追加しなさいよ、と書いてあります。

erlc -pa "path/to/eunit/ebin" $(ERL_COMPILE_FLAGS) -o$(EBIN) $<

しかしいざOTP R14Bの下のディレクトリを検索してみても似たような名前のものはあっても、eunitという名前のディレクトリはありません。

$ find /opt/erlang/R14B -name eunit.hrl
/opt/erlang/R14B/lib/erlang/lib/eunit-2.1.5/include/eunit.hrl

シンボリックリンクでも張ってんのかな?とid:kuenishiにSkypeチャットで聞いてみたところ下記回答。

[10/10/23 9:57:03] kuenishi: include_libは、ertsが適当にパスを読み替えてくれる

分かんねえよ...