Promiseの使い方が良く分からなくなってきた
先に言っておくと疑問文で終わる雑な記事です。
Promise、正常系の処理は比較的簡単に書けるのだけど、何か間違いがあったときに Promise 自体のエラーハンドリングが強力すぎて良く分からなくなる。
readFile
というファイルを読み込む Promise ベースの良くできた関数があるとして、
読み込んだファイルを処理するとき
readFile("foo.txt").then(function(text) { doSomething(text); });
ファイルが読み込めない場合を想定するとき
readFile("not-found.txt").then(function(text) { doSomething(text); }, function() { readFailed(); });
この時、catch
を使うのは良くなくて、ファイルの読み込みは成功したのに readFailedが実行される場合が考えられる。
readFile("存在するファイル.txt").then(function(text) { doBrokenSomething(text); // ここでエラーが発生する }).catch(function() { readFailed(); });
これを回避するにはこう書く。というか Promise ベースの API を使ったら最後に必ず catch
しておく必要があると思う。
readFile("foo.txt").then(function(text) { doComplexSomething(text); // ここでエラーが発生するかも知れない }, function() { readFailed(); }).catch(console.error.bind(console)); // ^^ doComplexSomething/ readFailed でエラーがあったとき
と、いうのが Promise の基本的な使い方だと思っているのだけど、、
これだとエラーがあったことは分かるけど、どこでエラー発生したか等の情報がいまいち分かりにくくて、デバッグしづらい。よくよく考えると Promise は非同期で実行されるフローを分かりやすく書くことが目的なので、すべてのおぜん立てが揃った最後の then
の中で複雑な処理を書くのがそもそもの間違いな気がする。その段階では Promise のエラーハンドリング機能は邪魔でしかないと思う。Promise の呪縛から逃れるには 大きなスコープ を使うか setTimeout
を使うというのが考えられるのだけど、本当にこんなので良いのでしょうか?
// 大きなスコープを使う var text = null; readFile("foo.txt").then(function(_text) { text = _text; }); button.on("click", function() { if (text !== null) { doComplexSomething(text); } else { readFailed(); } });
// setTimeout を使う button.on("click", function() { readFile("foo.txt").then(function(text) { setTimeout(function() { doComplexSomething(text); }, 0); }, function() { setTimeout(function() { readFailed(); }, 0); }); });
追記:
ということをブツブツ書いていたら、有益な情報教えていただきました!!
@mohayonao > 回避するのに then の中で setTimeout
Promise#done的なやつですね。
エラーの握りつぶし(unhandledRejection)が起きやすい問題は今実装進んでる最中という感じです
https://t.co/Yylri9lYEw
— azu (@azu_re) 2015, 3月 3
Promise を返す decodeAudioData をつくった
draft Web Audio API shim ブームということで、先日つくった StereoPannerNode の shim に引き続いて Promise を返す decodeAudioData をつくった。
decodeAudioData というのはオーディオファイルのバイナリ(ArrayBuffer)をAudioBufferに変換するメソッドでサンプリングベースのなんかそういうのをあれこれするときに使います。
今はこういうインターフェースをしている。
callback DecodeSuccessCallback = void (AudioBuffer decodedData); callback DecodeErrorCallback = void (); interface AudioContext : EventTarget { void decodeAudioData( ArrayBuffer audioData, DecodeSuccessCallback successCallback, optional DecodeErrorCallback errorCallback ); }
で、将来的にはPromiseだろうという感じで、古いインターフェースとの互換性を保ちつつ戻り値をPromiseにして、ついでにエラーコールバックのときに引数がつくような変更が予定されています。
callback DecodeSuccessCallback = void (AudioBuffer decodedData); callback DecodeErrorCallback = void (DOMException error); interface AudioContext : EventTarget { Promise<AudioBuffer> decodeAudioData( ArrayBuffer audioData, optional DecodeSuccessCallback successCallback, optional DecodeErrorCallback errorCallback ); }
Promiseベースのインターフェースは古いインターフェースとの互換性があるので今と同じ使い方を続けるというのもありだけど、他の非同期処理と同期をとりたいとき(Promise.all)などPromiseの文脈で書きたいときに、いちいち調べたり自前でラッピングせずに便利に使えます。
使い方
<script src="/path/to/promise-decode-audio-data.js"></script>
これで AudioContext.prototype.decodeAudioData がPromiseベースに上書きされます。
仕組み
こういう感じ。単純にPromiseラッピングして後方互換用のコールバックを登録しているだけです。
var decodeAudioData = AudioContext.prototype.decodeAudioData; AudioContext.prototype.decodeAudioData = function(audioData, success, error) { var audioContext = this; // Promiseでラッピング var promise = new Promise(function(resolve, reject) { decodeAudioData.call(audioContext, audioData, resolve, reject); }); // 後方互換用のコールバックを登録 promise.then(success, error); // Promiseを返す return promise; };
StereoPannerNode の shim をつくった
なんかカンファレンスとかあって一部で非常に盛り上がっているっぽいウェブオーディオ界隈ですが、そろそろ Chrome あたりに StereoPannerNode というのがやってきそうな気配があります。
今のウェブオーディオで音の定位を扱うには PannerNode というのがあるのですが、なぜか3D空間に音を配置するみたいな超高級な仕様で、パンといえばミキサーについているつまみくらいの感覚の一般市民には使いにくかったりします。
で、新しくステレオ定位を扱う StereoPannerNode というのが検討されています。
http://webaudio.github.io/web-audio-api/#the-stereopannernode-interface
interface StereoPannerNode : AudioNode { readonly attribute AudioParam pan; };
見てのとおりインターフェースは単純で、pan 属性があるのみ。-1 で左、0 で真ん中、+1 で右という按配です。 なるほど分かった簡単だし便利そうだ。ですが、代替できる仕組みがないとすべてのブラウザに実装されるまで使いたくないというのもあるので、今のウェブオーディオだけで作ってみた。
使い方
<script src="/path/to/stereo-panner-shim.js"></script>
これで AudioContext.prototype.createStereoPanner が使えるようになります。
var audioContext = new AudioContext(); var audioElement = document.getElementById("audioElement"); var mediaSource = audioContext.createMediaElementSource(audioElement); var autoPanRate = audioContext.createOscillator(); var autoPanAmount = audioContext.createGain(); var stereoPanner = audioContext.createStereoPanner(); // これが新しく使える autoPanRate.frequency.value = 0.05; autoPanRate.start(audioContext.currentTime); mediaSource.connect(stereoPanner); // 普通に接続して使います autoPanRate.connect(autoPanAmount); autoPanAmount.connect(stereoPanner.pan); stereoPanner.connect(audioContext.destination);
仕組み
既存のオーディオノードをごちゃごちゃ組み合わせてやっています。詳細はここの README を参照してください。
JSタイマー三種盛りとそのインターフェース
最近タイマーAPIを書くブームが来ていて、とりあえず3つほど書いたので順番に紹介していきます。
ブラウザのタイマーはタブの裏に入ると精度が悪くなる。それを解消する方法として WebWorker でタイマーを作動させてメッセージ送信→コールバック関数を実行するやり方がある。ウェブオーディオとか見えなくてもちゃんと処理してほしい時に使える。
setInterval で 100 を指定すると、だいたい 100ms 単位でコールバック関数が呼ばれるのだけど、これは 100ms ± 10% くらいのアバウトな間隔でコールバックされるやつ。"酔う"感じとかは設定で変えられる。
sinon の FakeTimer などのように、手動で時間を進められるやつ。テストに使う。
いずれもネイティブと同じインターフェースで setInterval
とか setTimeout
を呼んで使う。例えば、n回タイマー処理をしたい場合、以下のようなインターフェースにしておくと、状況に応じてタイマーを切り替えられるという算段です。
var nTimesInterval = (n, callback, delay, timerAPI = global) => { var count = 0; var timerId = timerAPI.setInterval(() => { callback(count++); if (count >= n) { timerAPI.clearInterval(timerId); } }, delay); }; // 普通のとき nTimesInterval(10, callback, 100); // バックグラウンドでも絶対に実行したいとき nTimesInterval(10, callback, 100, WorkerTimer); // ぐだぐだな感じにしたいとき nTimesInterval(10, callback, 100, DrunkTimer); // テストのとき nTimesInterval(10, callback, 100, TickableTimer);
APIを引数で受け取ると良いというのは前回書いた記事と同じ。
新しいタイマーを作るぞ!というときにオブジェクティブな感じで new して start / stop みたいなインターフェースにするのではなくて、ネイティブのに合わせておくと使用性が上がる。
具体的には関数名を同じにするのと、出来たらメソッドでなくて関数にする。例えば乱数を生成するのは Math.random
と同じように random
という名前でアクセスできて、fn = foo.random; fn()
ができた方が良い。
乱数を使う関数のテスト
function coin(x) { return Math.random() < x; }
こういう関数があったとして、どうテストを書くのか。
ぱっと思いつくのはこういう感じで、Math.random
自体を上書きするやり方。
describe("coin(x)", function() { it("works", sinon.test(function() { this.stub(Math, "random", function() { return 0.5; }); assert(coin(0.4) === false); assert(coin(0.5) === false); assert(coin(0.6) === true); })); });
このやり方、たしかに間違っていないんだけど、関数のインターフェース自体を変更した方が良いと思う。
function coin(x, random) { random = random || Math.random; return random() < x; }
これだとテストがしやすいだけじゃなく、偏った乱数生成器とかシード付きの乱数生成器を使用したりできて便利さ100万倍になる。
describe("coin(x)", function() { it("works", function() { var random = function() { return 0.5; }; assert(coin(0.4, random) === false); assert(coin(0.5, random) === false); assert(coin(0.6, random) === true); }); });
参照
JavaScript の this が分からない
ライブラリをES6で書いて公開する所から始めよう | Web Scratch
この記事を読んで、よっしゃ僕も練習がてらライブラリのES6化するぞ!!と思って、とりあえず既存ライブラリのテストだけでもES6化しようと思って脳内コーディングをしたところ、ライブラリ作者にとって Arrow Function が意外と厄介なんじゃないかと感じた。
(実際に試したわけではないので、全体的に勘違いしている可能性があります)
例えば、ちょっと雑いけど以下のようなテストコード。
describe("test", function() { it("Math.random", sinon.test(function() { // <- test sandbox this.stub(Math, "random", function() { return 0.5; }); assert(Math.random() === 0.5); })); // sinon.test を使えばテストが失敗しても書き換えたスタブは元に戻る });
sinon の sandbox は this を書き変えるので Arrow Function (this が外側のスコープに束縛される) は使えない。結果 function
と =>
が混在して嫌な感じがする。デンジャラスメル。
describe("test", ()=>{ it("Math.random", sinon.test(function() { // <- Arrow Function が使えない this.stub(Math, "random", ()=>{ return 0.5; }); assert(Math.random() === 0.5); })); });
これの対処は以下のどちらかになると思うのだけど。まあ後者だろうなぁと思う。
- ライブラリ使用者が Function式 と Arrow Function を使い分ける
- ライブラリ作者が
call
とかapply
を使わない
sinon のインターフェースを自分なりに修正するならこんな感じにすると思う。this に頼らず、それ相当のインターフェースを引数として別途提供するみたいな感じ。
describe("test", ()=>{ it("Math.random", sinon.test((sinon)=>{ sinon.stub(Math, "random", ()=>{ return 0.5; }); assert(Math.random() === 0.5); })); });
っていうか CoffeeScript の ->
みたいの、なんでないのだろう?
OSCを共有する osc-hub というのを作った
openFrameworks や Max/MSP, SuperCollider などで良く使われるメッセージングプロトコルに OSC (Open Sound Control) というのがあって、どんなものかについては この辺 を参照して欲しいのだけど、通信プロトコルにUDPが使われていて、基本的には A -> B というように一対一でしかメッセージを送信できない*1。
そこでOSCを中継するハブのようなものがあれば、複数端末にメッセージ送信ができて便利そうなので作ってみた。頑張って作った構成図とGIFアニメが以下。
使い方
具体的なコマンド等は README を参照してもらうとして、大雑把な流れを示す。
- あらかじめ OscHubServer を立ち上げる
- 各クライアントで OscHubClient を立ち上げて OscHubServer に接続
- Max/MSP とかで OscHubClient に設定したポートでOSCを送受信する
用途
インターネット越しに同じMaxパッチを共有して、スクリーン共有みたいな感じでOSCだけを共有して動作を伝えるとか、まったく別々のパッチを同じコントローラで制御するとかに使えると思う。たぶん便利なのでアイデア次第でいろいろ面白いことできそうだけど、便利すぎるのですでに同じようなのがあるのかも知れない。
留意点
ネットワークを介するのでレイテンシーが発生する。
*1:ブロードキャストアドレスを使えば、同一ネットワーク内の全端末に送信できる