Node.jsのコールバック地獄をPromiseやGeneratorを使って解消する
「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました! この記事はここでも公開されています。
Original article: Managing Node.js Callback Hell with Promises, Generators and Other Approaches by Marc Harter
下のようなコードが、親しみをこめて「コールバック地獄」とか「死のピラミッド」とか呼ばれているのはご存じですよね。
doAsync1(function () { doAsync2(function () { doAsync3(function () { doAsync4(function () { }) }) })
この状態がコールバック地獄かどうかは、意見の分かれるところでしょう。ネストがいくら深くても全く問題がないコードもあるからです。非同期のコードでフロー管理ができないほど複雑になってしまった場合は悪夢ですが。自分のコードがどの程度"ひどい"状態に陥っているかを確認するには、こう自問してみてください。「doAsync1
の前にdoAsync2
が実行された場合、リファクタリングにどこまで労力がかけられるか」と。ここでのゴールはネストの階層を減らすことではなく、モジュール化された(もちろんテスト可能な)、論理的で耐障害性の高いコードを書くことです。
この記事では、複数のツールやライブラリを使ってモジュールを作成し、どのようにフロー制御が動作するのかを検証してみようと思います。さらにN ode.jpの次バージョンで導入される予定のソリューションについても触れるつもりです。
問題
それでは、特定のディレクトリ内で最もサイズの大きいファイルを探すモジュールを実装することにしましょう。
var findLargest = require('./findLargest') findLargest('./path/to/dir', function (er, filename) { if (er) return console.error(er) console.log('largest file was:', filename) })
コードを書く前に手順をリストアップしてみます。
- 対象ディレクトリにあるファイルを読み込む
- ディレクトリ内の各ファイルのstatsを取得する
- サイズの一番大きいファイルを判定する(同じサイズのファイルが複数あれば1つ選ぶ)
- 最大のファイルの名前を返す
どこかでエラーが発生した場合は、ファイル名ではなくエラー値を返します。また、コールバック処理の呼び出しは一度だけとします。
ネストを使ったアプローチ
最初にネストを使ったアプローチを試してみましょう。もちろん"ひどい"ネストではありませんよ。ロジックを内向きに書いていくのです。
var fs = require('fs') var path = require('path') module.exports = function (dir, cb) { fs.readdir(dir, function (er, files) { // [1] if (er) return cb(er) var counter = files.length var errored = false var stats = [] files.forEach(function (file, index) { fs.stat(path.join(dir,file), function (er, stat) { // [2] if (errored) return if (er) { errored = true return cb(er) } stats[index] = stat // [3] if (--counter == 0) { // [4] var largest = stats .filter(function (stat) { return stat.isFile() }) // [5] .reduce(function (prev, next) { // [6] if (prev.size > next.size) return prev return next }) cb(null, files[stats.indexOf(largest)]) // [7] } }) }) }) }
- ディレクトリ内のすべてのファイルを読み込みます。
- 各ファイルのstatsを取得します。ここは並列処理されるので、I/O がすべて終了したかどうかを確認するために
counter
変数を用います。また、エラーが起こった場合にコールバック関数(cb
)が2度以上呼ばれないよう、ブール値errored
を使用します。 - 各ファイルのstatsを収集します。並列処理で配列にstatの値をセットしているところに注目してください(filesからstats配列への変換)。
- すべての並列処理が完了したかどうかをチェックします。
- 通常のファイルだけを取得します(リンクやディレクトリなどは除く)。
- リスト内で最大のファイルを特定するため、リストを絞り込みます。
- 特定したstatとコールバック関数を使ってファイル名を返します。
このアプローチで十分問題を解決できそうですが、並列処理の扱いと、コールバックが一度しか呼ばれないようにする処理は、注意が必要そうです。これらの注意点を扱う方法は後で触れることにして、まずは同じ処理を小さいモジュールに分けて考えてみましょう。
モジュールを使ったアプローチ
先ほどのネストを使ったアプローチは、以下の3つのモジュールに分けることができます。
- ディレクトリからファイルを取得する
- それらのファイルからstatsを取得する
- statsとfilesを処理して最大のファイルを決定する
最初のタスクは基本的にfs.readdir()
で事足りますから、わざわざ関数を書く必要はありませんね。まずは、順序を保持しつつ対象となるパスのstats
を返す関数を書いてみましょう。
function getStats (paths, cb) { var counter = paths.length var errored = false var stats = [] paths.forEach(function (path, index) { fs.stat(path, function (er, stat) { if (errored) return if (er) { errored = true return cb(er) } stats[index] = stat if (--counter == 0) cb(null, stats) }) }) }
さて次に必要なのは、statsとfilesを使って最大のファイルの名前を返す関数です。
function getLargestFile (files, stats) { var largest = stats .filter(function (stat) { return stat.isFile() }) .reduce(function (prev, next) { if (prev.size > next.size) return prev return next }) return files[stats.indexOf(largest)] }
では、これらを統合してみましょう。
var fs = require('fs') var path = require('path') module.exports = function (dir, cb) { fs.readdir(dir, function (er, files) { if (er) return cb(er) var paths = files.map(function (file) { // [1] return path.join(dir,file) }) getStats(paths, function (er, stats) { if (er) return cb(er) var largestFile = getLargestFile(files, stats) cb(null, largestFile) }) }) }
- ファイルとディレクトリからパスのリストを生成しています。
モジュールを使ったアプローチでは、コードが再利用がしやすく、テストも容易になります。メインのエクスポートが分かりやすいというメリットもあります。しかし、statを取得する並列処理の管理は自前で実装していましたね。これをフロー制御モジュールを使った処理に変更してみましょう。
asyncモジュールを使ったアプローチ
asyncモジュールは幅広く使われていて、Nodeコアにも親和性のある方法です。では早速、今回のプログラムをasyncモジュールを使って実装してみましょう。
var fs = require('fs') var async = require('async') var path = require('path') module.exports = function (dir, cb) { async.waterfall([ // [1] function (next) { fs.readdir(dir, next) }, function (files, next) { var paths = files.map(function (file) { return path.join(dir,file) }) async.map(paths, fs.stat, function (er, stats) { // [2] next(er, files, stats) }) }, function (files, stats, next) { var largest = stats .filter(function (stat) { return stat.isFile() }) .reduce(function (prev, next) { if (prev.size > next.size) return prev return next }) next(null, files[stats.indexOf(largest)]) } ], cb) // [3] }
- async.waterfallは、一連のフローを実行します。
next
というコールバック関数を用いることで、1つの処理で得たデータを一連のフロー内の次の関数に渡すことができます。 - async.mapは、取得したパスを使って非同期でfs.statを実行し、結果の配列を返します(順序は保持されます)。
cb
関数は最後のステップが完了した後、または途中でエラーが起こった場合に一度だけ呼ばれます。
asyncモジュールを使えば、コールバック関数は間違いなく一度しか呼ばれません。また、私たちの代わりにエラーを伝播し、並列処理を管理してくれます。
promiseを使ったアプローチ
promiseは、エラー処理と関数型プログラミングを得意とします。promiseを使って、今回の問題にアプローチしてみましょう。ここではQモジュールを使います(もちろんpromiseを使った他のライブラリを採用してもかまいません)。
var fs = require('fs') var path = require('path') var Q = require('q') var fs_readdir = Q.denodeify(fs.readdir) // [1] var fs_stat = Q.denodeify(fs.stat) module.exports = function (dir) { return fs_readdir(dir) .then(function (files) { var promises = files.map(function (file) { return fs_stat(path.join(dir,file)) }) return Q.all(promises).then(function (stats) { // [2] return [files, stats] // [3] }) }) .then(function (data) { // [4] var files = data[0] var stats = data[1] var largest = stats .filter(function (stat) { return stat.isFile() }) .reduce(function (prev, next) { if (prev.size > next.size) return prev return next }) return files[stats.indexOf(largest)] }) }
- Nodeコア機能はpromiseを意識しないので、認識できるようにします。
- Q.allはすべてのstatの取得処理を並列で実行します。その際、結果の配列の順序は保持されます。
- 次の
then
関数に引き渡すため、最後にfilesとstatsを返します。
これまでの例と違い、promiseチェーン(つまりthen
)内で投げられた例外はキャッチされて処理されます。では、クライアントAPIもpromise仕様に変更しましょう。
var findLargest = require('./findLargest') findLargest('./path/to/dir') .then(function (er, filename) { console.log('largest file was:', filename) }) .catch(console.error)
設計は上記のようになりますが、インターフェイスにpromiseを用いる必要はありません。多くのpromiseライブラリには、nodebackスタイルのコールバックも扱えるようにメソッドが用意されています。Qモジュールでは、nodeify関数がそれに当たります。
promiseのスコープについては、ここではこれ以上掘り下げません。詳細については、こちらをお読みになることをお勧めします。
generatorを使ったアプローチ
記事の冒頭で触れたように、Node v0.11.2から新機能が仲間入りします。そう、generatorです。
generatorはJavaS cript向けの軽量版のコルーチンです。yield
キーワードを使って関数を一時停止したり、再開したりできます。generator関数はfunction* ()
という特殊な構文を使います。その威力をもってすれば、prom iseや「サンク」を使った非同期処理を一時停止したり、再開したりすることもできるのです。 つまり"同期風"の非同期コードが書けるというわけですね。
「サンク」はコールバックを呼ぶのではなく、"返す"関数です。コールバック自体は一般的なnodeback関数と同じシグネチャを持ちます(つまり、第一引数はエラーです)。詳しくはこちらをお読みください。
それでは非同期処理を制御するフローに、generatorを活用した例を見てみましょう。今回はTJHolowaychuk氏のcoモジュールを紹介します。このcoモジュールを使って、今回の最大のファイルを返すプログラムを実装しました。
var co = require('co') var thunkify = require('thunkify') var fs = require('fs') var path = require('path') var readdir = thunkify(fs.readdir) <strong>[1]</strong> var stat = thunkify(fs.stat) module.exports = co(function* (dir) { // [2] var files = yield readdir(dir) // [3] var stats = yield files.map(function (file) { // [4] return stat(path.join(dir,file)) }) var largest = stats .filter(function (stat) { return stat.isFile() }) .reduce(function (prev, next) { if (prev.size > next.size) return prev return next }) return files[stats.indexOf(largest)] // [5] })
- Nodeコア機能は「サンク」を意識しないため、認識できるようにします。
- coはgenerator関数を引数に取ります。関数は
yield
キーワードを使っていつでも一時停止できます。 - generator関数は
readdir
が返るまで停止しています。結果の値はfiles
変数に割り当てられます。 - coは配列を扱う並列処理にも対応できます。結果の配列は順序を保持しつつ
stats
変数に割り当てられます。 - 最終結果を返します。
このgeneratorを使った関数は、記事の冒頭で紹介したコールバックAPIでも利用できます。coモジュールには優れたエラー処理能力があり、すべてのエラー(例外発生を含む)をコールバック関数に引き渡してくれるのです。またgeneratorでは、yieldステートメントをtry/catchブロックで囲むことができ、coモジュールではこの特性を利用しています。
try { var files = yield readdir(dir) } catch (er) { console.error('something happened whilst reading the directory') }
coモジュールは、配列、オブジェクト、ネストされたgenerator、promiseなどをサポートするたくさんの機能があります。
他にも新しいgeneratorモジュールが次々と登場しています。例えばQモジュールにはQ.asyncメソッドがあり、generatorを使うcoモジュールと同じような働きをします。
まとめ
今回は、「コールバック地獄」を解消するため、つまりアプリケーションのフローを制御するために、さまざまなアプローチを検証しました。個人的にはgeneratorを使ったアプローチがお勧めです。generatorが今後koaのような新しいフレームワークでどのような展開を見せるのか、楽しみですね。
サードパーティのモジュールを検討した箇所では触れませんでしたが、モジュールを使ったアプローチは、どんなフロー制御ライブラリ(async、promise、generator)にも適用することができます。今回のプログラムをよりモジュール形式に修正するは、どうすればいいでしょうか。使えそうなライブラリやテクニックを知っている方がいたら、ぜひコメント欄に書き込んでください。
この記事で紹介したコードのサンプル、その他のgeneratorの使用例をチェックしたい方は、GitHub repoへどうぞ。
StrongOpsを使ってNodeアプリを監視する
イベントループの監視、Nodeクラスタの管理、メモリリークの追跡に関心があるあなたに朗報です。ローカル環境でもお好みのクラウド上でも、すぐにStrongOpsを始められますよ。npmで簡単にインストールできます。
その他のおすすめ記事
- 次の Node v0.12 のリリースでは何が来るでしょうか?大幅はパフォーマンス改善についてはBen Noordhuisの記事をどうぞ。
- Bert Belderのビデオプレゼンテーションを見れば、v0.12でリリースされる機能について幅広く知ることができます。
- Node.js でAPIを開発し、あなたのデータを利用する準備が整いましたか?ローカル環境でもお好みのクラウド上でも、すぐに始められます。npmで簡単にインストールが可能です。
Pythonにサヨナラを
「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!
この記事はここでも公開されています。
Original article: Saying Goodbye To Python by Ian Bicking
ずっと先延ばしにしてきた記事を書きます。決別宣言ではなく(ずいぶん前に離れていますし)、ただ自分が歩んできた道を振り返ったに過ぎません。Pythonの世界に別れを告げてずいぶん経つのに、これまでサヨナラを言う勇気がなかったのです。
何年も前にPythonを卒業したとはいえ多少の愛着は残っており、戻る可能性もあると思っていました。PyCon 2013への提議が却下されたことは頭にきましたが(面白い話をしようと思っていたのに!)、この件で自分はもうPythonコミュニティの一員ではないのだと確信しました。
Pythonは私が初めて(もしかしたら唯一)参加したプログラミングコミュニティです。自らの意思でこの世界に入ろうと決意し、Pythonを選びました。大学にいた頃はSchemeとSmalltalkに興味があり、どちらも面白いアイデアの詰まった高尚な言語だと思っていました。ただどちらも実用的とは思えなかったのです。Schemeは常にライブラリが少ない状況でアカデミックコミュニティも貧弱でした。一方のSmalltalkはビルド好きがビルド好きのために作っていたので生産的ではありました(このことはアラン・ケイよりダン・インガルスの功績が大きいでしょう。ダン・インガルスの仕事には強い情熱を感じます。純粋だけど非生産的な言語では満足できなかったでしょう)。でもSmalltalkには今も昔も独自の世界がありました。その文化や技術はインターネット以前、オープンソース以前、オンラインコミュニティ以前と変わりません。すばらしい言語と環境だったとはいえ、当時は目新しかったアイデアを実現できませんでした。Smalltalkが変化に適応しようとしても難しかったのです。(私がSmalltalkと決別した理由は、過去のブログ記事「Smalltalkはどこで道を誤ったか」とその続きで、多少なりとも好意的に説明しています)
大学の卒業が近づき(1999年頃)、私は自分の基盤となりうる言語を積極的に探しました。PerlやCにも手を出しましたが馴染めません。そうこうするうちにPythonと出会い、これ以上の言語は必要ないと感じました。でもSchemeやSmalltalkにのめり込んだ時の感覚とは違います。SchemeはGW-BASIC以上に世界への扉を開いてくれたし、SqueakとSmalltalkはミステリアスで、まるで古代遺跡を発見したかのような興奮がありました。一方のPythonはただ実用的だったのです。でも当時の私は発見よりビルドを求めていました。
Pythonを使っていくつも仕事をしましたが、平凡なWebプログラミングをしているうちに、ビルドそのものよりビルドのためのツールをビルドする方に熱中し始めます。この頃にクールなプロダクトをいくつか作りましたが、完成したのはライブラリではなく、それより小さなユニットだけでした。そしてPythonのコミュニティを見つけます。
ビルドも続けました。Webwareに貢献していましたが、現在のPythonのWeb開発は別世代のもののように感じます。私はSQLObjectを開発し、「どうしよう、みんながこれを使っている。いいのかな」と思いながら、ライブラリに最初の一歩を踏み出しました。しかしSQLObjectはタプログラミングの概念をいくつも使ったせいでPythonの世界では奇抜な存在となり、維持すること自体がとてつもない重荷に感じられました。今後どうするか決断を下すまでかなり時間がかかり、(少なからず原因は自分の関心不足にあるのですが)かつてのような興味を失ってしまいました。それで私よりずっと信頼できるオレグにバトンを渡したのです。残念ながら私はこのパターンを繰り返していたのですが、SQLObjectが次世代に役立ったのならそれで満足です。
やがてWSGIが登場し、その巧妙さと実用性にワクワクしました。PasteでWebフレームワークツールキット(もしくはWebフレームワークを作るためのフレームワーク?)を丸ごとビルドしました。私がPasteで何をしたかったのか本当に理解していた人は少ないでしょうし、私もよく分かっていませんでした。世の中にはベン・バンガートのようにコードに隠された原理が分かるプログラマもおり、利用可能な状態にして公開する人もいました。(私もPJEに同じことをしていた気がします)当時、PythonのWebフレームワークはひどい状態でしたが、最終的には私がPasteで目指したブリッジの構築より、モノリシックなアプローチで成功に近づく方がうまくいきました。ここからどんな教訓が得られるのか分かりません。いかに状況を見極め、誰とどうやって問題を解決するのか、広い視点が必要です。ただようやく、様々な視点やスキルを統合したビジョンの重要性は分かった気がします。成功するには適切な人材を適切な環境に集め、連係させていく方法を学ばなくてはなりません。
この頃、Pythonでコーディングし始めて1万時間ほどに達したでしょうか。Pasteの教訓からライブラリ設計のヒントを得てWebObを記述しました。今でもこれがHTTPからPythonへの最善のマッピングだと思っています。他のライブラリにはもっとWeb開発の側面が含まれていたり、解説が整っていたり、ユーザが多かったりしますが、見方によってはWebObもまだ捨てたものではありません。WebObはWebフレームワークの構築を探究するための重要な要素となってきました。この時期、他のライブラリにはlxml.htmlや、小規模なWebTest、ScriptTest、MiniMock、Tempita、野心あふれる(あるいはただ間違った野心の)Deliveranceがありました。この頃は自分のシステムから多くのアイデアを消していたような気がします。
しかし、なぜか私のツールで最大の成功を収めたのがvirtualenvとpipでした。どちらにも最大限の情熱どころか、それに近い情熱すら傾けたことはありません。デプロイメント時のイライラを抑えたり(virtualenv)、Setuptoolsやeasy_installに対するユーザの不満をかわす(pip)のが目的でした。両ツールの微妙な成功は幅広い支持のせいかどうかは分かりません。ライブラリではなくユーザの目に見えるツールだったことが理由かもしれないし、誰も手をつけたがらず完成が待たれていただけなのかもしれません。
私の最後のプロジェクトはSilver Liningでした。DevOps革命初期の頃で、Webアプリケーションのための汎用コンテナを考察する試みです。ある意味、より深くvirtualenvとpipにのめり込むことになりましたが、単にバラバラのツールを寄せ集めるだけでなく、完全なブロダクトを作るのが目的でした。誰もSilver Liningのことなど気に留めていませんでしたが、私は少しだけ気になっていました。Silver Liningは、ずっと考えていたデプロイメントに関してアイデアが完成されていたし、Webアプリケーション開発では軽快で信頼性が高いと言われていて、やりたくないタスクを飛ばして自動化することもできたからです。それでもvirtualenvやpipほどの思い入れはありません。私はコードやテクニカルデザインの道を進みましたが、離れてみるとそれも平凡なものでした。
実際に離れたらPythonにはワクワクする要素は何もありませんでした。Pythonは順調でしたが、私の興味が時流に左右されることはありません。確かにPythonはかつてないほど成功しています(マイナーバージョンアップは別にして)。でも私は自分でツールセットを構築できるし、そのツールセットで作りたいものを作れると自負していました。具体的にどんなプロダクトかは考えていませんでしたが、適切なツールがあればそれなりの自信とスピードを持って行動できると思っていました。
でも私の場合、自分のアイデアに酔っている時は一度立ち止まらないといけません。冷静になって考え直した方がいいからです。そのまま突進してしまえば、きっと失敗して落胆するでしょう。もちろん実際のところは分かりません。思ったほどクールなアイデアでもなければ現実的でもないだろうと不安になり、目をそらしたくなるのかもしれません。それでも大抵は一歩下がってアイデアをより深く見つめ直してみます。ある意味、ツールやライブラリを作るのも、これと似たようなプロセスをたどった結果だと思います。すごいものを作ってやろうと思う気持ちが強すぎました。失敗するのではないか、完成できないのではないかと不安になったため、一歩離れてツールの開発をしようと思ったのです。
サーバでがんじがらめになっていても、私は常にWebに目を向けていました。ユーザの立場であっても、何とかしてネイティブGUIを避けていました。データの使い道も考えずにデータ処理をしても楽しくありません。それに運用は、とにかく最悪です。私は以前からフリーソフトやWebの信者だったので単なるフロントエンドとは思っていません。オープンソースでないWebも、Web向けでないオープンソースもあるでしょう。ただこうして書いていると、自分はWeb向けでないオープンソースに関心がないのだなと分かります。
Pythonでのコーディングをやめた時、もはやPythonはWebとは関係のないものに思えました。少なくとも興味のあるWebの範囲とは重ならないと感じたのです。私が作ってきたツールも今やWebとは無関係で、情熱も感じなくなっていました。データベースを使ったWebサイトやHTTPベースの動的なWebアプリケーション、テンプレートやデプロイメントといったRESTと呼ばれる部類のものには将来性を感じられず、自分が探し求めてきたものなど存在しないかのようでした。
これは新たな発見でも何でもありません。振り返ってみてそう思うのです。もし数年前にどうかと尋ねられたら、この意見に賛成したでしょう。自分で思いついたことではなく、むしろ世界が今のようになるのは分かり切ったことだったのかもしれません。こうしてJavaScriptやブラウザやDOMに目を向け始めたのです。
私がMozillaに加わったのはPythonから離れる少し前です。Mozillaへの参加がきっかけで、Pythonから距離を置くようになったわけではありません。もしMozillaにいながらPythonに携わっていれば、ずっと気楽だった可能性もあります。
移行してからここ数年は四苦八苦していました。Pythonやサーバに関わっていた頃は自分の仕事を理解していたし、得意な分野なので自信もありました。設計に関してどんな質問をされても自信を持って答えられたでしょう。周囲からは尊敬され、誰もが私の意見に耳を傾けました。ここまでのスキルを身につけるのに1万時間ほど費やしたと思います。
でもJavaScriptに移行してすべてが逆転し、今もかつてのレベルには達していません。もしWeb開発をやっていたら、もし別のプログラミング言語に移行した人が周りにいれば、もし何でもいいから小さなグループに入っていれば、これほど苦労しなかったのかもしれません。でもMozillaの環境は違いました。とはいえ、それで構わないのです。自分に自信があったのは、正してくれる人が誰もいなかったからでしょう。
不思議なことに、プログラマが数日や数ヵ月で新たな言語を身につける方法が話題になるのをよく耳にします。プログラマの知識は、(この例のように)別の言語に応用可能だと考えられているようですが、私は疑問を感じます。独善的な意見でしょうが、こう考える人たちは何かをマスターすることの実際を分かっていないのかもしれません。もちろん新たな言語や環境をモノにするのに、また1万時間をかける必要はないでしょう。それでも確実に数千時間は必要ですし、何年もかかることです。私自身はようやくそのレベルに近づいたと感じています。
何かに熟練することについて、恐らくこれが自分なりの考えなのだと思います。何かをやろうと決意して実際に行動するのはいいことですが、それだけで熟練の域に到達できるわけではありません。解決すべき課題を見つけないといけないし、それに対する正しい答えも見つけないといけない。計画の見直しが必要になれば、その原因を理解して適切なタイミングで軌道修正を図らないといけない。大局的でありながら細部にまで直感を働かせるべきだし、ささいなことで泥沼にはまらないようプログラミング言語のしっかりした知識も必要でしょう。でないと、大きなことに挑戦しようにも精神的なエネルギーがそがれてしまいます。PythonからJavaScriptへの移行はそれほど大きなことではないのもしれません。言語体系はよく似ているし、どちらもプラットフォームはブラウザです。それでも自分の直感を鍛え直して新しい環境でやっていくには、ある程度の時間がかかるのです。
残念ながら、私が以前の世界に戻ることはないでしょう。JavaScriptはPythonとは違います。まだJavaScriptのコミュニティを見つけていませんが、あったとしてもPythonを作り上げたPythonコミュニティのようなものはJavaScriptの世界に存在しないと思います。Pythonはインターネットから生まれた点がJavaScriptと違います。JavaScriptはインターネットのために作られましたが、Pythonはインターネットで作られました。今でもPythonコミュニティとその住人たちを懐かしく感じます。
ただ、今の自分にはプログラミング言語に対する忠誠心のようなものはありませんし、今後も気に入るような言語は出てこないでしょう。別に興味を失ったわけではありません。むしろ今でも最適なツールで仕事ができるよう頭を悩ませています。ただ議論の余地もないのにソフトウェア工学のタスクや選択肢に取り組むことが、何だか技術に対する冷めた諦めのように思えてきたのも事実です。
今は言語よりもプラットフォームに心引かれており、特に興味深いのがブラウザです。目新しいものではありませんが(ランタイムとして考えれば新しいとも言えますが)、その具体性や今日的な存在意義は大きな魅力です。今やブラウザはサーバからデータを受け取るだけの受け皿ではなく、サーバの代わりにサービスを仲介できる独立したエージェントとも言えます。もちろんそれがブラウザのすべてではありませんが、ひとつの将来的なモデルであり、未来のアーキテクチャを理解するには良い視点だと思います。
以上が私の目指している方向です。まだ道を歩き始めたばかりですが、完成できないようなツールを作って道草を繰り返すつもりはありません。現在はコラボレーションに取り組んでおり、手始めにTogetherJSを手がけました。次はもっと大きなプロジェクトにチャレンジしようと新たな実験も始めています。自分では、こうしたアイデアを形にするだけの能力をある程度身につけたと思うものの、プログラミングは大きなビジョンを推進するための一部分に過ぎません。サポートの集め方、プロジェクトの管理方法、実現可能性と利用者価値の両立、戦略とデザインの妥協点の探り方など、時間をかけて学ぶべきことはまだまだ残っています。それにコラボレーションそのものが専門知識の必要な領域です。これまでの知識をベースにある程度の仕事はこなせるものの、ここまでに挙げた項目で正しい選択ができるほどの経験はまだありません。努力してはいるものの、時間だけが過ぎていくようで不安です。
とにかく、これが今の私の立ち位置です。もうプログラミング言語の信奉者でもなく、どのコミュニティに属しているのかも定かではないので自己紹介にも困ります。プログラミングという技術的な基盤はあるものの、漂流者のような気分です。Python時代の仲間にサヨナラを言うのは名残惜しいので、代わりにこの言葉を。またいつかお会いしましょう。こちらの世界に来る人もいるでしょうから。
Olarkを使ったリアルタイムの技術サポート
「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!
この記事はここでも公開されています。
Original article: Real-time tech support with Olark by Miha Rebernik
ブラウザ上で動画の吹き替えができるアプリDubjoyは、パブリックベータテストを開始して以来、大きな進歩を遂げました。
現在Dubjoyのベータ版は、言語サービス・プロバイダや、声優、翻訳者、通訳者など、かなり限定されたターゲットにのみ公開されています。ほぼ一般人と言ってもいい、テクニカルな知識を持っていない人がほとんどで、彼らは問題が起きた時に効率よくデバッグすることも、問題を再現してオペレータに説明することもできません。
長期にわたりパブリックベータテストを実施した結果、今では問題のほとんどが解消され、ソフトウエアの品質も大幅に向上しました。
しかし、私たちは未だにユーザをサポートする最善の方法を探し続けています。それもできれば"リアルタイム"でサポートしたいのです。
ブラウザ上で吹き替え用の音声を録音するアプリには、動的なパーツが多く使われています。これらのパーツが、ユーザを次々と困った状況に追い込む原因となっています。
私たちはDubjoyを初めて使うユーザをサポートし、ソフトウエアの使い方を学んでもらう必要があります。作業手順やマイクの感度、権限やFlashのバージョンなどが課題になるでしょう。
ユーザとのコミュニケーションは、Webビジネスの重要なファクターです。コミュニケーションを上手く取ることができれば、ユーザの好感度は驚くほど上がります。
"いつでも助けてもらえる"という安心感が、ユーザの心をつかむのです。
必要な機能
次のような機能が備わっていれば、理想的なシステムと言えるでしょう。
- リアルタイムのフィードバックとヘルプ
- ログの転送
- エラーの転送
- 診断チェック
- リカバリ・ルーチン
- リセット・ルーチン
- システム情報の取得
そこで、こうした機能を持ったサービスがないか探してみました。
Olark
あなたがもしWeb上で何かを販売しているなら、ぜひOlarkを導入してチャンスをつかんでください。
Olarkは、自分のサイトに統合して使用できるチャット・ウィジェットです。Olarkをインストールすると、サイトを訪れたユーザがGoogle TalkのようなIM(インスタント・メッセージ)プログラム上に、チャットできる相手として表示されるようになります。
こちらからユーザをクリックしてチャットを開始することもできますし、助けが必要になったユーザが「Chat with us(チャットする)」ボタンをクリックするまで待機することもできます。
Olarkを導入するだけで、先ほど挙げた最初の機能、"リアルタイムのフィードバックとヘルプ"が手に入ってしまいました。
さらに、Olarkの開発者向けAPIのドキュメントを読み込んでいくと、このチャット・ウィジェットは簡単に拡張できることが分かります。会話を始める際のコーディングのように、カスタム・コマンドを作ってイベントと結びつけることができるのです。
Olarkの統合を検証する
Olarkには、チャット・オペレータがコマンドを実行できるという素晴らしい機能があります。
各コマンドには、!debugon
のように言葉の前にビックリマークを付加したシンプルな名前がつけられています。
統合をつかさどる"脳ミソ"は単純なパーサで、api.chat.onCommandFromOperator
イベントにフックされています。
簡単な例として、!explainer
コマンドの実装を下記に紹介しましょう。
olark 'api.chat.onCommandFromOperator', (event) -> # !explainer # Show the popup with the explainer video. if event.command.name is 'explainer' V.olark.send "[EXPLAINER] Showing popup" V.overlay.help()
また、現時点でDubjoyに統合されているコマンドは次の通りです(!dubjoyコマンド
を実行すると、その時点で統合されているコマンドのリストが表示されます)。
# !dubjoy # Show Dubjoy help and commands. if event.command.name is 'dubjoy' V.olark.send "[HELP] Dubjoy Olark integration\n" V.olark.send "!status - Status report (env, kombajn, video, frags, app)" V.olark.send "!debugon [<level>] - Turn on log fwd at a level" V.olark.send "!debugoff - Turn off log fwd" V.olark.send "!reload [<restore>] - Force reload and restore state" V.olark.send "!undo - Undo last step" V.olark.send "!explainer - Show the explainer popup" V.olark.send "!fragments - Output detailed fragments data" V.olark.send "!fragmiss - Output fragments that are missing audio" V.olark.send "!fragclear <fragment_id> - Clear audio data for fragment" V.olark.send "!fragseek <fragment_id> - Seek to fragment" V.olark.send "!diagnose - Run system checks" V.olark.send "!system <details> - Show system information" V.olark.send "!video - Show video information, such as YouTube ID, etc." V.olark.send "!mic - Show if mic access is allowed" V.olark.send "!audio - Show the average and latest audio levels" V.olark.send "!audiotest - Send the audio test URL and instructions" V.olark.send "!exec <command> - Execute command in V. namespace (e.g.: '!exec video.play()' would execute 'V.video.play()'"
基本的なものから高度なものまで、様々なタスクを実行するコマンドが用意されているので、ユーザをサポートすることも、診断テストを実行することも、システム情報をチェックすることもできます。
普通の文章よりコードを読む方が好きだというあなたは、Olarkの統合クラスをチェックしてみてください。
リアルタイムのフィードバックとヘルプ
Olarkは、アプリの操作中に行き詰まってしまったユーザやアプリを初めて使うユーザをサポートする際に特に力を発揮します。
例えば!explainer
コマンドを実行すれば、短い動画をユーザの画面にポップアップさせることができます。ユーザはその動画を見てアプリの使い方を学ぶことができるのです。
このようにOlarkを活用すれば、ユーザに"いつでも助けてもらえる"という安心感を与えることができるでしょう。サポートに満足したユーザは、サイトの初期バグくらいは見逃してくれるものです。
ログの転送とエラーの報告
アプリ上で何が起こっているかを把握するのに欠かせないのがログの存在です。イベント処理からビデオcurrentTimeの更新、音声エンコードの進行状況にいたるまで、履歴データはすべて細部にわたってログとして記録されています。
クライアント側で発生したエラーを、サイト側が瞬時にポップアップ警報として受け取るエラー報告も便利なツールです。エラーが発生すると、サイト側の全エンジニアの画面にチャットウィンドウが表示され、最初に対応したエンジニアがユーザの問題解決にあたります。
次のようなケースを想像してみてください。
あるユーザがDubjoyを使って動画を吹き替えたいのに、音声が録音できず困っています。
そこでユーザはOlarkのチャット・ウィジェットに「音声が録音できずに困っています」と書き込みました。
オペレータは「少々お待ちください」と回答すると同時に、Olark APTに統合されているカスタム・コマンド、!debugon
を実行してログの転送を開始します。
ログの転送を実施すれば、ユーザのconsole
からチャットウィンドウを経由することなく、すべての履歴データがオペレータの手元に送られてきます。もちろん、すべてがリアルタイムで処理されます。
このようにログ転送は、今ユーザが何をしてるのか、何をクリックしているか、またアプリがどのような動作をしているかを瞬時にして見ることができる非常に便利な機能なのです。
ログ転送の実行方法
私たちはアプリ全体で使用できる、中心となるログ・ルーチンをいくつか持っています。
ベースとなるログ・ルーチンを持つことでコードの差し替えが簡単になり、情報をOlark全体に送信することが可能になりました。ログ転送はデフォルトでは実施されませんが、オペレータは!debugon
コマンドを実行してログ・ルーチンのon
、off
を切り替えることができます。
次に示すのはOlarkと統合したログ・ルーチンのスニペットです。
# Log to Olark if V.olark if output_routine is 'error' # Log errors ALWAYS to Olark. V.olark.log type: 'ERROR', text: msg, args: args else if V.olark.debug and V.olark.debug is true # Forward logs to Olark if debugging is turned on. V.olark.log text: msg, args: args
また、ログ・ルーチンの典型的な使用例も紹介しておきます。
診断チェック
診断チェックは、コマンドひとつでユーザのシステムの基本診断を実行できる便利な機能です。
あるコマンドを実行するだけで、マイクの権限や感度の設定状況、Flashのバージョン、ネイティブMP4サポート、ビデオバッファの状況などをすばやくチェックすることができるのです。
では、実際に!diagnose
コマンドを実行している画面を見てみましょう。
さらにOlarkには、ユーザが行った直近の録音におけるゲインレベルを表示させる!audioコマンドのような、より詳細な診断が可能な音声診断コマンドも用意されています。そうしたコマンドを利用すれば、マイクが音を拾わない、感度が低すぎるといったトラブルにもすばやく対応することができます。
リカバリ・ルーチンとリセット・ルーチン
これらのルーチンは、ユーザがアプリを操作中に何らかの理由でエラーが発生し、オペレータがクライアントサイドでアプリを再起動する際に利用できます。オペレータはアプリの再起動時にデータをリカバリするのか、初期化したアプリを立ち上げるのかを選択することができるというわけです。
システム情報の取得
Olarkの標準機能だけでも、ある程度のユーザのシステム情報、例えばIMアプリのツールチップのブラウザバージョン情報などを入手することは可能です。
しかしOlarkには、より詳しい情報がほしい時に実行できる、下記のようなコマンドもあります。
!system
コマンドを使えば、ユーザのブラウザとシステムに関する有効な情報が入手できます。
また!video
コマンドを実行すれば、ユーザが操作している動画に関する主要な情報が手に入ります。
結論
ここまでの検証で、Olarkが私たち技術者やユーザを満足させる十分な機能を備えていることが分かりました。
Olarkの持つプラットフォームの拡張性を活用すれば、JavaScriptで実装されたあらゆるクライアントサイド・アプリへの対応が可能となり、結果としてユーザサポートをますます進化させることができるでしょう。
画期的な起業プロセス
「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!
この記事はここでも公開されています。
Original article: The Broken Startup Process by Emil Davtyan
2013年の初め、私は起業を決意しました。
それまでの4年間はいくつかプロダクトを開発し、大手クラシファイドサイト向けに運用していました。2008年に構築した画像ホスティングサイトは急速に成長し、ユニークビジター数が月200万になったこともあります。2011年には先ほどのクラシファイドサイト向けに検索や投稿ができるAndroidアプリを開発し、20万件以上インストールされました。現在は利用規約のためアプリの提供を終了しています。
この4年間、私はずっと1人で仕事をしていました。システム管理、データベース管理、PHP開発、Perl開発、Android開発、マーケティング、サポート、法務すべてです。手作業でヌード写真を削除し、Bindサーバのセットアップまで全部こなしていました。
対処すべき不具合も多く、1人でやっているときちんと対策する時間もほとんどありません。フリーのタフな開発者でも妥協ばかり続けているとモチベーションが下がってしまい、やたら非効率なのに小さなことに追われてしまうのです。かといって、あまり他社に依存すると開発者は食物連鎖の底辺になってしまう。だからこそ次はチームを作りたいと思ったのも当然でした。
ところがチームを作るにあたり、これまで以上に大きな難題にぶつかります。
最初は個人的なネットワークを洗い出すところから始めました。何年も1人で働いてきたことや内向的な性格が災いし、知り合いといえば数名の開発者がいる程度です。
そのうちチームに加えられそうなのは3人。コンピュータサイエンスを学んだ友人、物理を学んだ友人、そしてWebサイトを一度だけ作ったことのある友人の友人です。大学の卒業が近づいていたので、1人目の友人に起業を持ちかけました。彼は乗り気でしたが、修士課程への進学が決まっていたためリスクを冒すことはできません。2人目の友人も似たような状況で、3人目には声をかけませんでした。
知り合いが少ないのは自分のせいです。何をしてきたか聞かれてもつい説明を避けてきましたし、飲みすぎた夜でもなければAWSへの情熱を公言することもまずありませんでした。人と交流するのは苦手ですが、それでも落ち着ける場所から踏み出さねばならなかったのです。
そこでMeetUp.comに登録しました。Webテクノロジと起業に関するグループを探し、ミーティングの写真に圧倒されながらも、イベントへの参加を2件予約しました。
どちらの会場にも、熱い意気込みの人、人材を探している人、ピザを求めてウロウロしている人がいます。私は会場を歩き回っていくつかのグループに飛び入り参加したのですが、会話を聞いていてすぐに分かりました。ほとんどの参加者がプログラミング初心者か、一切経験がなかったのです。主催者を除くとプロのプログラマはごくわずかでした。それも、とうに野心を失った年上の人たちばかりです。
私にはかなり不利な点もありました。当時住んでいたのはテキサス州ダラス。テクノロジに強い人材が少なく、仲間にできそうな人を見つけて説得するだけで数カ月かかりそうでした。いっそのことサンフランシスコに引っ越してスタートアップに参加し、カンファレンスに出席してネットワーク作りをしてから自分のやりたいことをやろうかとも考えました。でも、そんなプロセスを踏むなんてバカげていますよね。
たった一言のツイートで革命を起こせる時代に、起業プロセスを最初から最後までやろうとしていたのです。
でもWebなら人材を配分できる最高のシステムを作れます。安く速く構築できて、誰でも加われる。このようなネットワークを持っている企業はすでにありますが、閉鎖的で非効率です。ネットワーク外でなら誰もが参加できますが、それだと多くのベンチャーは投資家を説得できないでしょう。ネットワーク内の人材でさえ、大きなリスクを吸収できるかはベンチャーキャピタルの意向に左右されます。
この問題が現実となりました。そこで私は真っ向から取り組み、解決策を探り始めたのです。ベンチャーキャピタル抜きでも人が自由に集まり、アイデアを形にできる場所を作れないだろうか? 誰でも空き時間に参加して仲間を募れるような場所、ダイナミックな労働力が集まる場所です。
このアイデアを具体化するため、1人で6カ月以上かけてアルファ版を作りました。この時点でパートナー探しは中止しています。
私はPythonにもDjangoにも詳しくなかったので、Stack Overflowのスレッドに何度も助けられました。役に立つ回答をもらえたら、回答者のプロフィールをブックマークしておき、今度はインターネットの力に期待して1人ずつコンタクトを取りました。連絡先を公開している人に、「面白いプロジェクトがあるけど、もし興味があればSkypeで話さないか?」とメールを送ったのです。
およそ100人のうち15人から返事がきて、その中の10人とSkypeで話しました。私のアイデアをすぐに理解してくれた人もいましたし、山ほど質問をして納得してくれる人もいました。やり取りしながら彼らの表情を読み解くのは刺激的です。アイデアに潜む問題点も、話を理解してくれたことも感じ取れるからです。解決策を提示し、間違っているかもしれないので一緒に問題を片づけていこうと誘いました。
これには朝から晩までやって1カ月半ほどかかりました。ほとんどの人は最初のメールに返事もくれませんでしたし、私の話に納得して協力したいと言っていた人たちですら実際には動いてくれませんでした。しかも仲間を探し回っていた間は、忙しくてコーディングも進んでいなかったのです。プロジェクトは遅々として進まず、このままでは立ち行かないと思うこともありました。多くの時間を費やしたのに、アイデアを伝えられたのはわずか10人。仲間を1人見つけるには、1,000人に声をかけないといけないのでしょうか。どうやら私も、陥りがちなワナにはまっていたようです。アイデアを大切に守るあまり、その価値を高めないまま潰しかけていました。
残された道はただ1つ、アイデアをオープンにするしかありません。つまり世界にドアを開いて自分のアイデアや不完全なコードをさらけ出し、意見や協力を広く募るのです。
こうしてJoltem.comは誕生しました。「joltem」は「jolt them(彼らを驚かせろ)」の略で、私たちは「オープンインキュベータ」と呼んでいます。少なくとも起業から初期の段階をオープンにすることで、多くの企業は利益を得られるとの考えに基づいています。オープンでダイナミックな労働力を組織し、人材を補うために作りました。
Joltemはコントリビュータ全員が1つのリポジトリで作業できるよう、GitHubで普及した従来型の pull&fork モデルは採用しないことにしました。1つのリポジトリで作業できれば大規模なコラボレーションにもすぐに対応できます。シンプルなgit fetchコマンドを実行するだけで、コントリビュータ全員からすべてのコードを集められます。また、コントリビュータごとにリモートリポジトリを追加する必要がなく、常に1つのdevelopブランチもしくはmasterブランチに集中できるのです。
これを実現するためにタスクとソリューションに基づくタスク管理モデルを開発し、ブランチレベルのパーミッションを設定できるカスタムのgitサーバと関連づけました。権限のあるユーザーはすべてのブランチにプッシュでき、他のコントリビュータは自分に割り当てられたソリューションブランチにだけプッシュできます。
報酬については、従来のストックオプションプールを少し変えています。一般企業だと資金調達ラウンドごとに一定数の株式が新入社員に割り当てられます。社員には一定数の株式が約束され、べスティングプランの対象になります。もしその企業で働き続ければ約束された株式の一部を受け取れます。Joltemでは会社が設立されるとその株式の一部がコントリビュータに割り当てられます。Joltemプロジェクトの場合は株式の85%で、従来のストックオプションに相当します。唯一私たちが変更したのは、プールの配分だけでした。
各コントリビュータがどれだけプロジェクトに貢献したかはインパクトと呼ぶ指標で測ります。コントリビュータがプロジェクトに何か提供するたびに、本人の判断でインパクトを自己申告します。他のコントリビュータはその人が満足のいく仕事をしたか、適切な自己評価だったかどうかを判定します。意見の食い違う場合は交渉プロセスで妥当な値を決めることになります。Joltemプロジェクトでの経験から言うと、意見の食い違いはめったに起きません。コントリビュータは自分の仕事にどの程度の価値があるか、感覚をつかんでいくからです。べスティングプランの場合と同じように、獲得したインパクトは一定期間を置いて、設立された企業の株式と交換できます。
さあ、Joltemなら誰でも参加できます。
ぜひプロジェクトを投稿してくだい。メンバーに声をかけて個人的にコラボレーションすることも、完全なオープンプロジェクトにすることも可能です。Joltemプロジェクトにだって参加できますよ。
JoltemのTwitter、Google+、Facebook もあります。私たちのredditr/joltemから、新プロジェクトをチェックしてみてください。
質問やご意見はemil@joltem.comへどうぞ。
JavaScriptでbind()を使って部分適用する
「HackerNews翻訳してみた」が POSTD (ポスト・ディー) としてリニューアルしました!
この記事はここでも公開されています。
Original article: Partial Application in JavaScript using bind() by Pascal Hartig
JavaScriptの中にはコードをもっとシンプルで見やすくできるパターンがあるのに、あまり使われていないものがあります。皆さんもFunction.prototype.bindはご存じでしょう。頻繁に使われていたvar that = this
やvar self = this
の代わりになる関数です。よくあるのが以下のような例です。
this.setup = function () { this.on('event', this.handleEvent.bind(this)); };
第1引数がbind
(束縛)され、返される関数内でthis
として働きます。あまり知られていませんがbind
は複数の仮引数を取ることができ、bind
された関数が呼び出されるとbind
される後続のすべての仮引数は、その仮引数リストの前に付加されます。
つまり以下のように、関数を部分適用することができるのです。
var add = function (a, b) { return a + b; }; var add2 = add.bind(null, 2); add2(10) === 12;
すごいでしょう。冒頭に例として挙げたイベント処理コードを拡張する場合など、この利点がよく分かります。他にもイベント処理の一般的なパターンとしては、ハンドラを呼び出す時にコンテンツを指定するというのがあります。
this.setup = function () { this.on('tweet', function (e, data) { this.handleStreamEvent('tweet', e, data); }.bind(this)); this.on('retweet', function (e, data) { this.handleStreamEvent('retweet', e, data); }.bind(this)); };
仮にtweet
とretweet
のイベントハンドラが似かよった論理構造の場合、このようにコードを書くのもいいでしょう。ただし欠点は一目瞭然で、ボイラープレートコード(似ているのに省略できないお決まりのコード断片)だらけです。両方に無名関数を用意しなければなりませんし、それぞれ内部でイベントハンドラを呼び出して引数を受け渡し、関数をbind
してthis
コンテキストをきちんと設定しないといけません。
もう少しシンプルにできないものでしょうか? もちろんできますよ。
this.setup = function () { this.on('tweet', this.handleStreamEvent.bind(this, 'tweet')); this.on('retweet', this.handleStreamEvent.bind(this, 'retweet')); };
これならスッキリしますね。無名関数内で関数を呼び出す代わりに部分適用された関数を2つ用意し、thisコンテキストとそれぞれ異なる第1仮引数を両方に指定しました。もちろんe
やdata
も問題なく渡されます。
もし皆さんが数カ月前の私と同じなら、ショックで自分の書いたコードから似たような箇所を探してきてはクリーンアップしたくなることでしょう。その作業が終わったら、このことを友達にも教えてあげてください。