qsonaのブログ

プログラマーです。

Node.jsだけで2年半生きてきた自分が、そろそろRuby on Railsをやってみようと思う話

Node.jsをやってきて良かったこと

職業プログラマとして2年半、Node.jsだけをやってきた。Node.jsで特に良かったと思えるのは以下の2つかなと思っている。

OSSへの貢献

大層なことではないが、この1年で、lodash, neo-async, xto6, などいくつかOSSへの貢献ができたこと。

2年目に新規開発チームに異動し、自分がチームで最もNode.js経験が長いという状態になった。弊社はGitHubEnterpriseを利用していて、このプロジェクトではメンバ全員がForkして基本はPull Request駆動(ガチガチではないが)という、基本OSSにそのまま応用できる形でやっていて、ここでgitのコマンドもきちんと使えるようになった(2年目にして初めてgit rebaseやgit cherry-pickを使った)。

ちょうど、我々の開発中にlodash3系が出そうということで、npmではなくGitHubの最新に向けておいた。おかげでいくつかバグを踏んだりもして、issueやPRを送るのはほぼ必然だった。ユーティリティ系のライブラリだから決してハードルは高くないが、それでも大御所に貢献できるのは嬉しかったし、自身の成長を感じることもできた。それ以外にも、いろんなライブラリのソースコードを読むのは日常的だった。

趣味レベルからOSS貢献に繋げられる人はすごいなと素直に思う。でも、業務のレベルでしっかり使うと、それだけ色々な課題も見えやすいし、OSS貢献のハードルは下がると思う。OSS貢献は一つの分かりやすい技術レベル指標だと思うので、それが出来やすい環境にいたのは良かった。

プログラミングの力がついた

Railsはプログラミングじゃないみたいな記事が最近あった。それはともかくとして、Node.jsはそれ自体microだしフレームワークもその大体が薄いから、自分で考えて実装する量が多いと思う。ブラウザゲームのサーバサイドは定形処理みたいなのが少ないし、フルスタックな言語やフレームワークを生かせないという判断で、Node.jsを使ってきていたのだろうと思う。いずれにしても、課題に対して実際にコードを書いて解決するのは自分にとっても性にあっていたし、いい経験になったと思う。

あとは、非同期処理が当たり前、というのもよかった。

そろそろRailsをやりたいという話

今なら先人のフルスタックな知恵を十分理解できるんじゃないか、と思ったということ。もうだいぶ前から、サーバサイド開発といえば筆頭に上がる存在だし、今だってそうだ。それほど価値を認められているものを、このまま知らずにいるのはもったいないんじゃないかと思った。今ここでRailsを十分知ることは、Web技術者としての視野を拡げることに繋がると思っている。

これからも仕事はNode.jsだけど、一旦趣味レベルでRuby on Railsをはじめてみよう。

みちすじ

やるやる詐欺にならないように。一旦、勉強しながら、簡単なゲームをRailsで実装してみようかと。ついでに最近流行りのマイクロサービスっぽく、フロントサーバとバックエンドサーバを分離してみよう。

作るもの

7 hand pokerという、昔MSN Messengerにあったゲーム。ユニークなルールですごい好きだったんだけどMSN Messengerつかわなくなってゲーム自体もやってないから、自分が遊びたいというモチベーションがある。あと、一度対戦ゲーム(トランプとかボードゲーム的な)をちゃんと実装したいと思ってた。

構成

ブラウザ(js) <=> Node.js(リアルタイム, ルーム機能とか) => Rails(7 hand pokerのロジック+REST? API) => MySQL

作らないけど、例えばチャットからやりたかったら chat client <=> bot server => Rails => MySQL みたいな感じに、Railsからは完全にそのまま流用できるイメージ。作らないけど。

いつもいつも(プログラミングに限らず)企画だけ立派にやって完成せずに終わるので、ここに書いておけば、出来なかったらちょっとは恥ずかしい、だからがんばる、という狙い。rubyも書いたことないしrailsも起動しかしたことないので、まずは手を動かしてみる。

東京Node学園祭2015 にて発表を行いました

発表前記事

東京Node学園祭2015 にて発表を行います - qsonaのブログ

プレゼン資料・補足記事

qsona.hatenablog.com

応募まで

2010-11年ごろのピグライフでの事例以来、弊社(サイバーエージェント)では特にゲームのサーバサイド開発に積極的にNode.jsを投入してきており、サーバサイドで動かすために書かれたNode.jsのコードの量では、国内だったら弊社はかなり多いんじゃないかと思っています(一番多いまで言えるかは不明ですが)。ただ、開発の多さに比べると、発信の数が少ないと思っていて、この機会に何か発信できるものはないかと考えました。今はそんなにNode.jsの特別な使い方をしているわけではないし、特徴はコードの量なので、その中での苦労話が最もリアルに話せて価値を出せるのではないかと思い、LT枠に応募しました。

前日まで

その後、古川さんから返信を頂き、面白そうな内容なので、LTではなくセッションでちゃんと話した方が良いのではないか、という非常に有り難いご提案を頂きました。自分にとってはこれ以上ない良い経験をさせて頂ける機会で、感謝しております。さすがに背伸び感はあるものの、Node.jsのコード書いてる時間ならそうそう負けないぞという気持ちで引き受けた次第です。

1つ前のNode学園で LT をして(これも良い経験でした)、その後学園祭1週間前までは発表の大枠を練り、そこから資料作成と練習にとりかかりました。準備の時間配分のバランスが難しいのですが、30分というのは結構なボリュームで、資料の作成にもっと時間見ておかないとダメだったのと思いました(20時間くらい?)。LTの時で分かっていましたが、喋ってみると資料がしゃべりにくい順番になっていることが分かるのが結構多くて、特にトークが主体になるような発表だとトライアンドエラーで良くする必要があるので、時間はしっかりとらないなと思っています。

余談ですが、今回の招待講演以外のセッションでは特に好評を博していた id:amagitakayosi さんがかなり事前準備を一生懸命されていたようで、印象的でした。

Node学園祭で「フロントエンドに秩序を取り戻す方法」を発表した #nodefest - マルシテイアは月の上

前日夜〜当日発表まで

会場が弊社だったので前日夜に行ってみたところ、スタッフの方々がみんな集まってくださっていて、自分も資料折りと差し込みを少し手伝いました。あまりの分量で変なテンションで折っていましたが、ああいった場で少しでも普段話せない社外の方と話す機会があるのは良いですね。

※左の写真の、一番右でなんか折ってるのが自分。

準備が終わった頃、titoさん(弊社の先輩です)にプレゼン資料見せてと言われたので、発表練習付き合ってくれるならいいですよと半分冗談で言っていたら、織田さん(同じく弊社の先輩)と2人で本当にフルでの発表練習に日付変わるくらいまで付き合ってくれました。良いアドバイスをもらえたので、発表としての体裁は大分整って、当日も言いたいことが流れの中ですんなり言えたと思います。

当日、スピーカーにはお弁当を用意して頂いていて、招待講演の方々と一緒にわいわいランチ・・・する余裕もなければ英語力もなかったのですが、写真をとらせてもらったりと、良い思い出になりました。

発表の趣旨

中規模以上のサーバサイド開発にNode.jsを利用する上で、特にプログラミング上で苦労している部分や、あまり良くないことをしている部分にスポットを当てながら、問題提起をしようと考えていました。

自分のやっていることから離れたところまで話すのは難しいし、現場のリアルさに欠けてしまうので、「ゲーム開発」「ES5まで」の2つの制限の上で話すことにしました。

最近ではマイクロサービスというのが一つのキーワードになっており、適材適所に言語やフレームワークを使い分けることが可能になっていると思います。サーバサイドでは一般的にはRuby(on Rails)が多く、Node.jsの出番は特にリアルタイム性が絡む時に多い(僕の直前の @kidach1 さんの発表もそうだった)というのが自分の印象ですが、弊社の今自分が属している部門(ゲーム)では要件にかかわらず普通に、アプリケーションのメインとなるAPIサーバをNode.jsで作り始めることが多いです。

で、特に何も決め手の特徴がなければRubyあるいはPHPとなっているところに、Node.jsが入っていくことは出来ないのか、どんな壁があるのかっていうのを最近考えていて、その上では、Node.jsでのサーバ開発の辛さ的なものの共有は一つの切り口になるかなと思っていました。

資料upを遅らせた理由

発表のテーマである「バッドノウハウ」というのは、単に良くないから改善すれば良いという話ではなく、良くないとは分かっていながらも仕方ない的なニュアンスが含まれています。自分としてはそういう内容で3つ選んだのですが、そもそも解決手法を十分に考えられていないだけではないか、というご指摘を受けました。そうだとしたら単に良くないものを喧伝する真にバッドな物になるため、ご指摘を受けた部分に関しては、出来る範囲で検証してから上げること、またその後も随時指摘を頂き訂正できるよう、補足記事とセットで上げることにしました。

感想と反省点

Twitter等での反応を見る限りでは、共感を得た部分もあったとは思いますが、聴いて下さった方に十分プラスのものを与えられるようなセッションにすることが出来なかったなと思っていて、謙虚に反省しなければならない点が多かったと思います。

反省点の一つは、まだまだ考察が足りなかったこと。バッドノウハウというタイトルでしたが、ノウハウ共有系とは正反対で、現状の良くないところを見ながら問題を提起していくということで、本質的なところに迫りきれなかったのは残念なところです。

もう一つは、ES5までの話と決め打った部分に無理がありました。Node4が出てわりとすぐでの学園祭ということもあり、直asyncで書かれたコードを全部書き直すのは大変だし、既存プロジェクトでgenerators(co)導入とかはまだ難しいかなという感じでそこまで重要に思ってなかったのですが、この1日では完全に時代に置いて行かれている感じしかしませんでした。yield使えっていうツッコミも結構頂いていましたね。そもそも書き方やパフォーマンスなど諸々検証するチャンスはNode v0.11の途中からもう2年くらいあったわけで、それを怠ってきたところから反省しなければなりません。

あとはまあ動画が公開されてたので自分のものを見てみたのですが、何か気の触る話し方だなと思いましたw もうちょっと謙虚な感じに聞こえるような話し方にしたいですね!

動画はここです(すべての発表の動画があります) 東京Node学園祭2015 | 株式会社サイバーエージェント

一番人気だったkosamariさんの発表と時間が被っていましてリアルでは見れず。私も動画で見ましたが、非常に面白いプレゼンでした。

他のセッションなど

NodeDiscussionが一番面白いコンテンツだったと思いました。その場で即興で話している英語なので聴きとる難易度が高かったですが、 kosamariさんの素晴らしい逐次翻訳で全て理解することが出来ましたし。初めての試みとのことでしたが来年からも期待しています!

あとは、他の方々の発表を聞いていて思ったのは、発表の内容自体はすぐにはピンとこなかったりしても、その方がどのように考えて開発や技術選択などをしているのか、ということが節々から分かって、どれも面白かったです。

全体的にはNode.js自体というよりはその周辺技術とかJavaScriptに関係する話が多く、もっとNode.js本体に関する話も聞いてみたいなという気もしました。ドストレートにNode.jsの話だったのは、自分が聞いた中ではNodeDiscussionと @kidach1 さんのセッションだけだったかもしれない。

終了後

懇親会では色々な方とお話できて、勉強になりました。とにかく疲れがどっと出て、帰って15時間寝たあげく2日間頭痛が続いて辛かった。ものすごく問題意識が高まったり、非常に良い刺激になった1日でした。

運営の方々や登壇者の方々、スポンサーの方々、ありがとうございました!

「Node.jsでのゲームサーバ開発 愛すべきバッドノウハウ3選」の補足

本エントリは、東京Node学園祭2015でセッション発表いたしました「Node.jsでのゲームサーバ開発 愛すべきバッドノウハウ3選」についての補足記事です。

スライドはこちら:

speakerdeck.com

発表動画も上げて頂いています:

youtu.be

発表の概要

発表内容は概ね以下のようなものです。

  • 動機付け: Node.jsをサーバサイドでもっと普通に利用したい, その上での
  • バッドノウハウ1. (ES5まででの)非同期フロー制御
  • ノウハウ. マスターデータの利用
  • バッドノウハウ2. "サービス層" になんでも記述する行為
  • バッドノウハウ2.5. Dateを上書きする
  • バッドノウハウ3. process.uncaughtExceptionをlistenしてエラーを握りつぶす

現場の規模感

自分が約1年間開発に携わったプロジェクトでは、サーバサイド開発者が平均5人程度で、大体がNode.js未経験からスタートしています。普通のWebアプリケーションのサーバサイドの開発と言ってよいと思います。コードが約10万行くらい(テスト除く)。「フツウ」のサーバ開発、と形容しているのはこれをイメージしています。

BAD 1. 非同期フロー制御について

発表中、最も自分が言いたかったことを言い切っている章ですが、反応としては「yieldで解決するのでは」というものが多かったです。Node v4でのgeneratorsがサポートされ、そしてasync/awaitの文法が今後入ることがほぼ確定しており、それらを含んだ話ができずにスライドの中で「決定版が待たれる」のような曖昧な言い方で濁したのは、自分の怠慢だったと言わざるを得ません。

1年ほど前にcoを調査したときは、callbackを取る関数はthunkifyして使うものだと思っていましたが、最近見たらthunkifyはdeprecatedになり、Promisifyして使うべき、となっていました。Promiseが標準化したため、正しい選択といえますが、気分的にはthunkに比べてPromise経由はどうも遠回りに思えてしまいます。「フツウ」を標榜しているところが気にするべきことではないですが、オーバーヘッドはどうしてもthunk以上に気になるので、実コードを一部移行して検証してみようと思っています。

それと気になっているのが、非同期を同期スタイルにすることで、callbackスタイルからtry-catchのエラーハンドリングになることです。

1つは、V8のoptimizationは気にする必要がないレベルなのかどうか(場合によってはtry節を全部即時関数で囲ったほうがいいのか)ということ。

もう1つは、非同期エラーも、主にバグで出される同期エラーもいっしょくたにcatchされてしまうことになると思うので、その辺を区別する(上手な)方法はあるのか?という疑問があります。ここに関してはそもそも区別する必要がないんじゃないかという話も頂いていて、実質的にはユーザのアクセスによって到達するようなコードなのでそう問題にならないはずですが、とはいえdomainがdeprecatedになった理由なども考えると、あるべき論としては、この2つは分けて考えなければいけないのかなと思っています。

諸々検証して、中規模以上の開発で使えるプラクティスを構築できればと思います。

最後に現場での話をすると、io.jsは利用しておらず、Node v4への移行は大体出来る状態まで持ってこれた(ただし文法の移行はまだ)という状態です。varをconstやletに置き換えるなどは出来るでしょうが、asyncを利用した既存のコードが多い状態で、generatorsを利用した記法と共存させるのは規模感的にも難しいかなというイメージですが、うまく共有または移行をしていきたいと考えています。

BAD 2.5. Dateを上書きする

自作ライブラリ qsona/time-master · GitHub を紹介しましたが、 sinonjs/sinon · GitHub のFakeTimerを使うべきではないか、という指摘を懇親会で頂きました。自分もsinonをテスト上で使っていますが、このような用途で使うことは全く考えもついていなかったし、テストでFakeTimerを使ったことがなかった(ユーザのアクセス時間をアプリケーション中で使っているので、基本的にそれを書き換えていた)ので、調べてみました。

sinonのFakeTimerは、ある決まった時刻で固定する用途として使えるようです。プレゼン中では話せていませんでしたが、我々の(テスト以外の動作確認の)用途としては、ある時刻に移動させたあと、そのまま時間が流れていって欲しく、そのような機能はFakeTimerにはないようでした。

最後に

この記事は随時追記します。間違いや疑問点等あれば、ご遠慮なくコメント欄でご指摘いただければ幸いです。

async.waterfallとneo-async.angelFallの話

今回登場するモジュールはこちらです。

github.com

github.com

簡単に説明すると、Node.jsにおいて、callback地獄とif (err) return callback(err);の多用を軽減して読みやすくするためのモジュールです。

async.waterfallについて

async.waterfallは、非同期な処理を逐次行っていく時に、メインに使うメソッドです。

使い方

var async = require('async'); // or 'neo-async'
async.waterfall([
  function(next) {
    next(null, 1);
  },
  function(result, next) {
    // result に1が入る。
    next();
  }
], function(err) {
  // 途中でnext(err)すると、ここに飛んでerrが入る。
}

問題点

次のように、第2引数を返したり返さなかったりするメソッドがあったときに、その次の関数の受け方が定まらなくて、よくバグの原因になる。

function anAsyncMethod(foo, callback) {
  if (foo) {
    callback(null, 1); // [A]
  } else {
    callback(null); // [B]
  }
}

async.waterfall([
  function(next) {
    var foo = false;
    anAsyncMethod(foo, next);
  },
  function(result, next) {
    // [A] の場合はresultに値(1)が入るが、
    // [B] の場合は入らない。(すなわち、resultにnextと呼ぶべき関数が入っている)
    next(); // TypeError: next is not a function
  }
], function(err) {
});

回避方法はある(後述)ものの、面倒だし、
「値を返していない状態から、返すように変更する」ことがbreaking changeになるのも辛い。
同期関数だったり、非同期コールバックスタイルでも普通の使い方であれば、そうはならないのに。

回避方法

anAsyncMethodがコールバックへ引数渡してきたり渡さなかったりするから困るなー、でもanAsyncMethod自体は書き換えられないな、という時は、次のように回避する。

async.waterfall([
  function(next) {
    var foo = false;
    anAsyncMethod(foo, function(err, result) {
      next(err, result); // 1つ渡すことを強制している
    });
  },
  function(result, next) {
    // ...

neo-async.angelFall

waterfallと似ているが、上述の問題が解決されたもの。

常に最後の引数にnext(と呼ぶべき関数)を渡す。

すなわち、関数の仮引数を、単に(next)と書けば、前の関数が何を渡そうが無視されて、nextに関数が入る。
もし(result, next)と書けば、仮に前の関数が何も渡してなければresultにundefinedを入れ、nextに関数が入る。

var async = require('neo-async');
async.angelFall([
  function(next) {
    var foo = false;
    anAsyncMethod(foo, next);
  },
  function(result, next) {
    // 定義してる引数の個数が見られ、
    // 2つなので、1つめに前の関数から渡される結果、2つめにnextが入る。
    next();
  }
], function(err) {
});

前回の記事で紹介したFunction.lengthを利用して実現されている。

注意点

async.angelFall([
  function(next) {
    var foo = false;
    anAsyncMethod(foo, next);
  },
  function(result) {
    // nextを呼ばないから仮引数としても定義したくないよ、というとき。(実際あり得る)
    // 仮引数を1つしか定義していないので、ここにnextと呼ぶべき関数が入ってしまい、
    // 期待する値(result)が入ってこない。

他の解決案

github.com

waterfallのタスク関数はfunction(foo, next)ではなく、function(next, foo)の順で取るように仕様変更するという話。

これをすれば、fooがこようがこまいが、nextの位置はずれないので上のような問題は起きない。

実際自分もこれは1年半前くらいに思ったのだけれど、Node.jsの「コールバック関数は最後にとる」という規約に反するので少し気持ち悪い。でも黒魔術しないですむし、async.waterfallに限って言えばこれもありだなぁと思う。

まとめ

Node.jsがcallbackを最後にとるというルールが、思わぬ形で影響を及ぼしてしまっている話でした。

Function#length と、そのlodashにおける使用例

JavaScriptにおいて、関数の中でarguments.lengthは良く目にする/使うと思うのですが、関数fn自体のfn.lengthを利用することはまれではないでしょうか。
(これ以降fnは全て、定義された関数を表すことにします)

async.angelFallを社内に布教しようと思ったのですが、そういえば前のエントリで言及しかけていたので、どうせならfn.lengthの話題とまとめてブログに書こうと思った次第。

fn.lengthとは

Function.length - JavaScript | MDN

arguments.lengthが関数が実際に呼ばれた時の引数の個数なのに対して、fn.lengthはその関数fnが定義されたときの仮引数の個数を表します。

var fn = function(a, b, c) {};
fn.length // 3

Function#bindで第2引数以降を渡すといわゆるカリー化ができますが、きちんとこのlengthの値も変わったりします。頭良いですね。

var fn = function(a, b, c) {};
// 引数aに1を部分適用した新しい関数を作る
var fn_curried = fn.bind(null, 1); // b, cの2つの引数をとる関数になっている
fn_curried.length // 2

使用例

コイツの使用例をいくつか紹介したいと思います。

lodash

まずは大御所lodashにおける使用例です。関連するissueはコチラ。(読まなくても下で全部説明しますが、読んで分かったら多分このエントリにそれ以上のことは無いです。)

https://github.com/lodash/lodash/issues/944

https://github.com/lodash/lodash/issues/997

さて、この問題の背景としていくつか説明する必要があります。

iterateeに渡る引数

まず、lodashのeachやmapなど(多数)のメソッドに第2引数として渡す、関数(lodashではiterateeと呼ばれています)は、次のように引数を3つ取ることが出来ます。

var arr = ['a', 'b', 'c'];
_.map(arr, function(value, index, array) {
  // value: 配列の値。 順に 'a'/'b'/'c' が入る
  // index: 配列のindex。 順に 0/1/2が入る
  // array: 配列全体。 ここでは毎度 ['a', 'b', 'c'] が入ってくる
});

これはJavaScriptArray#forEachなどと同じ仕様です。

lodashのメソッドチェーン

次に、lodashにはメソッドチェーンをする書き方があります。例としてはこんな感じ。(少し煮え切らない例ですが)

var arr = [1, 2, 3, 4, 5];
var result = _(arr) // _.chain(arr)でも同じ
  .map(function(value, index, array) {
    return value * value; // 2乗
  })
  .filter(function(value, index, array) {
    return value % 2 === 1; // 奇数
  })
  .take(2) // 配列の最初から2つを取得
  .value(); // 実際の結果を取得

result // [1, 9]

lodash v2までは、単に順番に _.map, _.filter, _.takeを適用していくだけでした。すなわち、以下と基本同じです。

var arr = [1, 2, 3, 4, 5];
var mapped = _.map(arr, function(value, index, array) {
  return value * value;
});
var filtered = _.filter(mapped, function(value, index, array) {
  return value % 2 === 1;
});
var result = _.take(filtered, 2);

result // [1, 9]

lodashの遅延評価

しかしlodash v3で遅延評価(lazy evaluation)が登場したことで少し話は変わります。

上記の例でいうと、配列をまずmapして[1, 4, 9, 16, 25]を得て、その後filterして[1, 9, 25]を得たうえで、takeで頭2つを取って[1, 9]を得ています。しかしながら、最初から「頭2つを取る」とわかっていれば、全て計算しなくとも、map・filterを手前からしていって[1, 9]まで得られた時点で打ち切っても良いことになります。

メソッドチェーンの方法で書けば、文として切れ目がないので、必ずしも[1, 4, 9, 16, 25]とか[1, 9, 25]という結果を得る必要はないわけです。そこで、メソッドチェーンの書き方をされた時に、可能であれば遅延評価をするような実装がなされました。

一方、後者の例ではmappedやfilteredという変数に一旦代入しているため、当然結果を計算する必要があります。(残念ながら、JSのエンジンレベルの遅延評価はありませんから・・)。これがlodash v3で実装された遅延評価の優位性です。

ちなみに、遅延評価時の実行の流れは大体こんな感じです。map=>filter=>takeの時点では、内部的に状態を持つだけで、計算はしません。そして、value()が実行されたタイミングで、以下のような手順で計算されます。

  1. arr[0]である 1 をmap(2乗)する。1
  2. 結果の1 にfilter(奇数)の関数をかける。 true
  3. trueなので、結果の配列にpushする。[1]。※take 2なのでまだおわらない。
  4. arr[1]である 2 をmapする。 4
  5. 結果の4 にfilterの関数をかける。 false
  6. falseなのでなにもしない。
  7. arr[2]である 3 をmapする。 9
  8. 結果の9 にfilterの関数をかける。 true
  9. trueなので、結果の配列にpushする。[1, 9]。 take 2なのでここで終了。[1, 9]を返す。

これにより、頭3つ分しか計算せずにすんでいます。

普通は1回ずつ配列を生成して次に進むのに対して、
遅延評価が効く場合は、配列の要素1つずつがchainを通り抜けていくイメージですね。

さて、これで背景の説明は終わりです。すでにお気づきでしょうか?

遅延評価で生じた、iteratee第2・第3引数の問題

map=>filter=>takeの真ん中、 filter に渡したiterateeの第3引数arrayに注目してみます。

本来ならばここには、直前でmapした結果である [1, 4, 9, 16, 25] が入るのが正しいはずです。 しかし、上の遅延評価の動作を見れば分かるように、mapした全体の結果 [1, 4, 9, 16, 25] は作っていません。ということは、第3引数のarrayには正しい値が渡ってこないことになります。(作ってないものは渡しようがない!)

普段はこの第3引数などまず使いませんが、_.uniqを遅延評価に対応させるPRを送ろうと実装をしていてたまたまこのことに気付き、issueを立てました。(拙い英語ですが)

その後、もう片方のissueで、第2引数のindexも正しさが保証されないことがわかりました(より重要な問題)。

fn.lengthを用いて修正される

これに対して、jdalton 氏が機転の利いた対応を行いました。

上の例では、遅延評価してしまうと、そもそもiterateeに第2引数のindexや第3引数のarrayを正しく渡すことができないのです。しかし、ほとんどの用途では第1引数のvalueしかとらず、そのときは問題ない。第2引数以降を渡さなければいけないケースだけ、遅延評価をとりやめれば良いのです。すなわち、iterateeの仮引数の数を見れば良い。それがこのコミットです。

Disable lazy optimizations if the iteratee has more than one param. [… · lodash/lodash@b872bc9 · GitHub

iteratee.length > 1のとき、isLazyfalseになっているのがわかります。

これを見た当時、fn.lengthはこうやって使えるのかと、心の底から感心しました。自分がissueを立てた時点ではこの方法は全く思いつかなかったです。

以上、lodashにおけるfn.lengthの利用例の紹介でした。(長かった。)

fn.lengthを利用する潜在的な問題

次の2つのコードの違いはなんでしょうか。

var fn = function(a) { return a; }
var fn = function() { return arguments[0]; }

答えはもちろん「fn.lengthの値が違う」です。(他にもfn.toString()の結果が違うなどありますが)

fn.lengthはあくまで仮引数の個数なのですが、argumentsを使えば、仮引数として宣言されていなくてもその引数にアクセスすることは可能です。
lodashの例でいけば、仮引数を宣言せずにiteratee内で第2・第3引数にarguments[1] arguments[2]などとアクセスした場合には結局、期待された値が入らないことがあります。

この問題はfn.lengthを使うと常に潜在してしまいます。

最後に

普通にプログラミングしてても絶対使わないようなものも、ライブラリ書くときにはたまに出くわすよね、という良い例だと思います。実際のプロジェクトでも、基盤的な機能の整備をするときには色々知っておくと便利そうです。
あと、lodash は結構面白いなと思います。

async.angelFallの話を書こうとしてたはずなんですがね。次で書く。

LT「チーム開発においてNode未経験者の学習コストを下げるための工夫」の補足

先日、東京Node学園18時限目「Node.js 4.0の話」の回で、LTをさせていただきました。

Node学園 18時限目 Node.js v4.0の話 - connpass

資料はこちらです。

speakerdeck.com

たかが10分といえど、120人*10分=20人時間というプレッシャー。勉強会等で発表するのはこれが初めてで、この1週間はひたすらこのLTのことだけを考えて生活していたような。
テーマは初めに思い切って設定したものの、まずはこの1年の開発を振り返り何がポイントだったのかを考えるところから・・・、
そこからプロットを作って話す練習をしてみると、どうも話が繋っていなかったり、技術的な話が少なすぎて、これじゃNodeの会合に来ているお客さんに申し訳ないよなぁ、というような内容だったりと、内容をまとめ上げるのに苦心しました。

そんな苦労もあった準備の甲斐もあり、良い反応も頂き、ひとまずホッとしている次第です。

会場にいらした皆様、ご清聴ありがとうございました。

さて、資料には一応載せたけれど、発表時にもスライド上でも十分説明できていない点について、このエントリで少し補足をさせていただきたいと思います。

lodashのバージョンについて

lodashはv2.4.1でしばらくの間リリースが止まっていましたが、約1年前くらいにv3がリリース、そしてそろそろv4がリリースされそうです。

特徴として、v2 => v3, v3 => v4 でのbreakingな変更が割と多いです。運用に入ってしまうとメジャーバージョンを挙げるのは結構大変になってしまうイメージです。
資料にちらっと書きましたが、いまから開発始めるなら、v4が出るまで(出てからしばらくまで)、依存先をedge(github上のmasterの最新)にしておくのが良いんじゃないかなと思っています。

v4の特徴としては、今まで1つの関数にいくつかの引数の渡し方ができていたところを、関数自体を分けるように変更がなされます。
例えば_.uniqという関数は、コレクションを渡すと重複する値を1つだけにした配列を返しますが、この_.uniqの引数に関数を与えると、その関数を通した結果に対して、uniqをかける、という仕様でした。
これが、_.uniq_.uniqByという2つの関数にわかれ、関数を渡したければ後者を使うことになります。

このように、シンプルな方向に倒していくのがlodashの良い所の一つであります。

その他、aliasを大量に削除するようです。underscoreにあったメソッドに対して、lodash v3で別の名前をつけておいて、元の名前にはaliasを貼っていたが、v4でそのaliasを削除するなんてケースも。。そこまでやるか・・??(lodashの作者jdaltonとunderscoreの作者jashkenasは仲悪いです)

なお、バグはそれなりにあります。中規模くらいでガッツリ使っていると出くわすこともあると思います。特にv3の初期は、遅延評価まわりのバグが結構ありました。issueを軽く追っておいたほうが良いです。

async#waterfallとneo-async#angelFallの話

ここらへんは、書き方オタクとしては結構こだわりのあるところで、キチンと書いておきたい話なのですがまた今度の機会にします><

lodashのクエリの書き方

オブジェクトが並ぶ配列arr中から、プロパティxの値が2であるものを見つけるとき。 _.find(arr, function(obj) { return obj.x === 2; })のがunderscoreからある書き方ですが、これを
_.find(arr, { x: 2 })という書き方ができます。さらに別の書き方として、
_.find(arr, 'x', 2)という書き方もあります。面白いですね。

個人的には2つめの_.find(arr, { x: 2 })が良いかなと思っています。
3つめにしない理由は、例えば「xが2, yが3のものを探す」ときに_.find(arr, {x: 2, y: 3})という書き方ができますが、3番目の書き方だと応用が効かなくなってしまうからです。

余談ですが、lodashあるあるで、_.includesを使うべき場面で間違って_.someを使ってしまう、というのがあります。
前者は単なる厳密な比較で含まれていればtrueを返す、後者は上の_.findと同じ感じで、見つかったらtrueを返す。

あるオブジェクトそのものがある配列に入っているかを調べたく、つまり_.includes(arr, obj)としたいところで、
間違えて_.some(arr, obj)なんて書くと、オブジェクト同士のプロパティを1つずつ(しかもdeepに)比較しはじめてしまう。
大抵の場合で結果が一致してしまうのでテスト書いてもバグに気づきにくい。

結果、思わぬところでパフォーマンスの問題を引き起こしてしまうかもしれませんね。
何度も見かけた事例ですが、lodashオタクならコードレビューで間違いに気付くことが出来ますw

undefinedチェックの話

LTで最も反応が良かった、undefinedチェックの話です。

資料中では以下の4点の方法を挙げています。

  1. x === undefined
  2. x === void 0
  3. typeof x === 'undefined'
  4. _.isUndefined(x)

ちなみにLT中で会場の方にどれを使うか挙手でアンケートをとらせていただきましたが、大体挙手の比率は 6:4:10:1 くらいだった印象です。

さて、自分はtypeof x === 'undefined'が良くないと考えている旨を話しました。その点を整理したいと思います。

まず、typeof x === 'undefined'は1つのJavaScriptのイディオムです。この書き方をすると、もし仮にxがそもそも変数として定義されていない場合でも、ReferenceErrorを吐くことがなく、上の式の結果はtrueになります。ブラウザ上で、特にグローバルに変数xが宣言されているかどうかをチェックする場合などに有用であることから、よく使われます。ReferenceErrorを吐かないから安全、という言われ方をすることもあります。

しかしながら、Node.jsではグローバル変数をチェックすることはまずありません。そのときに、「ReferenceErrorを吐かない」ことが「安全」なのか?という疑問が生じます。

例えば変数名をtypoしてしまった。そのときはReferenceErrorを吐くのがむしろ正しい動作で、そうならないということは、吐かれるべきエラーを握りつぶしていることに他ならないわけで、それによりミスに気付く機会を逸するかもしれません。"良くない書き方"であると考えるのはその点です。

さて、書き方をどれにするかでいうと、少し抵抗ある方もいるかもしれませんが、今時は x === undefined で問題ないと言ってよいでしょう。
一応var undefined = 1;されることもあるよ(だけどそんなことする人居ないから気にしないよ)と話しましたが、
Node 4.xで'use strong;'するとそれもできなくなるそうです。

というわけで、一番シンプルなx === undefinedで良いんじゃないでしょうか。

書き方オタクが複数人いたら?

これに対する、自分の解決策は以下です。

  1. 最終決定をする人(リーダー)は1人にする。
  2. 「書き方が統一されている」ことが最重要であり、 全ての書き方オタクはそれを肝に銘じている。

1の方は、書き方に限った話ではないでしょう。とにかく決定する人が2人いると面倒この上ありません。成功事例とした開発も、初期にダブルリーダーのような状態になったときは良くなかった。

2の方は、私のLTで極意の3つめとして話しているものです。これは他をさしおいてなによりも重要だと私は考えています。

良い実例があります。
僕がいっしょに仕事した中での代表的な書き方オタクに、neo-asyncという人物(?)がいます。ちょうど僕の前にLTを行った人です。

neo-async

suguru03/neo-async · GitHub

LT 「Neo-Async」

suguru03.github.io

僕と彼はしょっちゅう書き方について議論をします。その結果、50%は最終的に意見が合致しますが、意見が合わずに終わることも50%あります。

例えば、この記事の上のほうでlodashのクエリの話をしていますが、彼は3つめの_.find(arr, 'x', 2)という書き方が良いと言います。彼は何か話すときは常にベンチマークを取っており、確かこの件ではそれのほうが2倍くらい速く、わざわざ速い書き方があるのでそれを放棄する必要もなかろう、という主張でした。

私はというと、どうせ2つめの_.find(arr, { x: 2 })の書き方は覚えなければいけないので、(_.find(arr, { x: 2, y: 1 })のパターンがあるから)、学習コストを減らしたいので2つめで統一したい。

どちらが正しいということはないと思います。

それ以外にも本当下らないことでしょっちゅう言い合っています。_.each_.forEachかなど、自分の会話でなければ即座に「わりとどうでもいい」のAAを貼ってやりたいくらいです。

以前同じチームで仕事したときはたまたま、私がもともといるチームに彼が助っ人的に入ってくれていたこともあり、結果的には最終的には私の意見をほぼ100%通す形になってしまいました。もちろん彼の中では、自分が良いと思う書き方が出来ない不満はあったのではないかと思います。コードレビューでは、"殺してでも うばいとる"ではなく"ゆずってくれ たのむ!"にしたいゆえんですね(LT資料の終わりの方を参照されたし)。

もちろん意見は言ってくれますが、これ以上はその人の考え方あるいは気分だろうな、というところで、それに合わせて修正してくれていました。それはつまるところ、合わせるのが最も大事だと考えているからです。逆に彼のチームに私が入る形だったら、私のほうが合わせることを意識したと思います。(彼はカナダに英語勉強の旅に出てしまうので、少なくとも当分一緒に仕事する機会がないのは残念ですが。)

あとは、あまりロジカルでない主張をむりやり通すと良くなくて(そういうことも実際あった)、個人的にはわりと細かいことでもちゃんと話すのが良いのかなと思っています。ちょっと大変ですが、書き方がバラバラになるというリスクはそれほど大きいということです。

async書き方比較のソースコード

asyncの書き方の例を3つ挙げて比較するスライドを入れましたが、文字の情報にするためにこちらに載せておこうと思います。

準備

var async = require('neo-async');
var getFoo = function(callback) {
  return callback(null, { x: 1 });
};
var getBar = function(x, callback) {
  return callback(null, x + 1);
};
var doBaz = function(callback) {
  return callback(null, 'piyo');
};
var doQux = function(foo, bar, callback) {
  return callback(null, 'baz');
};

plain (callback hell)

var ex_plain = function(callback) {
  // fooを取得する.
  getFoo(function(err, foo) {
    if (err) {
      return callback(err);
    }
    // fooをごにょごにょする.
    foo.hoge = 1;
    foo.fuga = 2;
    // barを取得する.
    getBar(foo.x, function(err, bar) {
      if (err) {
        return callback(err);
      }
      doBaz(function(err, result) {
        if (err) {
          return callback(err);
        }
        foo.piyo = result;
        doQux(foo, bar, function(err, baz) {
          if (err) {
            return callback(err);
          }
          // ...
          callback(null, { foo: foo, bar: bar, baz: baz});
        });
      });
    });
  });
};

async.series (弊社で多い)

var ex_series = function(callback) {
  var foo, bar, baz;
  async.series([
    function(next) {
      // fooを取得する.
      getFoo(function(err, _foo) {
        if (err) {
          return next(err);
        }
        foo = _foo;
        // fooをごにょごにょする.
        foo.hoge = 1;
        foo.fuga = 2;
        next();
      });
    },
    function(next) {
      // barを取得する.
      getBar(foo.x, function(err, _bar) {
        if (err) {
          return next(err);
        }
        bar = _bar;
        next();
      });
    },
    function(next) {
      doBaz(function(err, result) {
        if (err) {
          return next(err);
        }
        foo.piyo = result;
        next();
      });
    },
    function(next) {
      doQux(foo, bar, function(err, _baz) {
        if (err) {
          return callback(err);
        }
        // ...
        baz = _baz;
        next();
      });
    },
  ], function(err) {
    if (err) {
      return callback(err);
    }
    callback(null, { foo: foo, bar: bar, baz: baz });
  });
};

async.waterfall 1 (推奨)

var ex_waterfall = function(callback) {
  var foo, bar;
  async.waterfall([
    function(next) {
      // fooを取得する.
      getFoo(next);
    },
    function(_foo, next) {
      foo = _foo;
      // fooをごにょごにょする.
      foo.hoge = 1;
      foo.fuga = 2;
      // barを取得する.
      getBar(foo.x, next);
    },
    function(_bar, next) {
      bar = _bar;
      doBaz(next);
    },
    function(result, next) {
      foo.piyo = result;
      doQux(foo, bar, next);
    },
    function(baz, next) {
      // ...
      next(null, { foo: foo, bar: bar, baz: baz });
    }
  ], callback);
};

async.waterfall 2

var ex_waterfall2 = function(callback) {
  async.waterfall([
    function(next) {
      // fooを取得する.
      getFoo(next);
    },
    function(foo, next) {
      // fooをごにょごにょする.
      foo.hoge = 1;
      foo.fuga = 2;
      // barを取得する.
      getBar(foo.x, function(err, bar) {
        next(err, foo, bar);
      });
    },
    function(foo, bar, next) {
      doBaz(function(err, piyo) {
        next(err, foo, bar, piyo);
      });
    },
    function(foo, bar, piyo, next) {
      foo.piyo = piyo;
      doQux(foo, bar, function(err, baz) {
        next(err, foo, bar, baz);
      });
    },
    function(foo, bar, baz, next) {
      // ...
      next(null, { foo: foo, bar: bar, baz: baz });
    }
  ], callback);
};

東京Node学園祭2015 にて発表を行います

2015/11/7(土)に東京Node学園祭2015が行われます。会場は今年も弊社サイバーエージェントのオフィスになります。

nodefest.jp

さて今回、「Node.jsでのゲームサーバ開発 愛すべきバッドノウハウ3選」というタイトルで登壇させていただくことになりました!

ここでは少し発表内容の背景を説明したいと思います。

弊社のゲーム部門では、サーバサイド開発にJavaとNodeを利用しています。開発者や事例が育ってきたこともあり、最近では新規開発にはほぼNodeを利用しています。
Nodeそれ自体の動作や開発のスピードに関して、困ったことはほとんどありません。(Nodeは普通のサーバ開発に十分適している、と私は考えているゆえんです)

さて、プログラミングには、一般的に良い/悪いとされている設計手法や原則が存在します。しかしながら、新卒で入社後、実際にゲームサーバ開発に従事したとき、必ずしもそれが実践されていないと感じることがありました。

最初は単に疑問に思うだけでしたが、(当然経験の深いエンジニアが多い中、新卒の自分などでは思い至らないことばかりなわけで、)そこから約2年の開発経験を通して自分なりに考えてたり試してきた結果、少なくとも「JavaScript」「Node.js」「ゲームサーバ」というコンテキストにおいて必ずしも"悪"と切り捨てられるものばかりではなく、良いあるいは致し方ない面がある、というふうに納得できるものが多くありました。
(単に直すべきポイントもあり、実際にこの1年ほどの開発で改善してきたものもありますが、それは今回の話では触れません。)

そのようなものを、今回「愛すべきバッドノウハウ」と呼び、そのうちのいくつかの事例に対して極力深い考察を与え、話したいと思っている次第です。中には自分が考案したバッドノウハウも含まれます(笑)

私自身、新卒でサイバーエージェントに入社して以来、仕事としてはNode.jsでの開発しか行ってきませんでしたし、プログラミングに対する一般的な考え方を十分に身につけているとはいえませんから、少し背伸びした発表になってしまうでしょう。
それでも、国内でのNode.jsサーバ開発実績としては最大に近い規模であろう我々の開発現場の工夫を、なんとかして発信していきたいという思いから、今回発表させていただくに至りました。

ところで、今回のタイトルは「・・・愛すべきバッドノウハウ3選」とありますが、現在自分の頭の中にあるものは、大〜小あわせて10個くらいあります。その中でより興味深いものを選んで3つ程度にする予定ですが、個数は少し前後するかもしれません。発表を作る上でさらに深く考えるうちに、もしかしたら単なるバッドノウハウに消化されてしまうものもあるかもなぁ、と思っています。それはそれで自分にとっては有意義なことですが。

自分の経験が、Node + ゲームサーバ開発なので、タイトルではそこに限定していますが、おそらくNodeサーバ開発全般に共通する話が70%くらいになると思います。

また、話の流れ上、タイトル通りのバッドノウハウだけでなく、Node+ゲームサーバにおける通常のノウハウ(グッドノウハウ?)も少し紹介できるかなと思います。

というわけで、興味のある方は聞きに来て頂けたら幸いです。
(なお、東京Node学園祭の参加者募集開始は10/13(火)0時からです。)