Edited at

LiveScriptのBackcallsでチェインする書き方(あるいは、しない)

More than 5 years have passed since last update.

LiveScriptは所謂AltJSの一つで、Cocoという


"to be more radical and practical"


CoffeeScript語族のforkのようです。ぱっと見では、記法をHaskellに大幅によせたCoffeeScriptという印象です。

言語についての解説としては、こちらが参考になると思います。

http://d.hatena.ne.jp/mizchi/20120706/1341568588


Backcallsとは

LiveScriptには"Backcalls"という記法があり、以下のようにJSでありがち?なコールバックのネストをフラットに記述することができます。


sample0.ls

do

dataFoo <-! $.get 'http://example.com/api/foo'
alert JSON.stringify dataFoo
dataBar <-! $.get 'http://example.com/api/bar', dataFoo
alert JSON.stringify dataBar
dataBaz <-! $.get 'http://example.com/api/baz', dataBar
alert JSON.stringify dataBaz


sample0.js

$.get('http://example.com/api/foo', function(dataFoo) {

alert(JSON.stringify(dataFoo));
$.get('http://example.com/api/bar', dataFoo, function(dataBar) {
alert(JSON.stringify(dataBar));
$.get('http://example.com/api/baz', dataBar, function(dataBaz) {
alert(JSON.stringify(dataBaz));
});
});
});

というか、要はHaskellのdo記法っぽく書くためのエミュレーションですね。「<-の左のオペランドを引数とする無名関数が、右のオペランドの関数呼び出しの引数末尾あるいはプレースホルダー("_")の位置に展開される。無名関数の中身は次行以降doでスコープされる範囲まで」という変換がされると理解しています。

上の例の場合明示的にdoを記述していますが、この後にコールバックのネストから外れる処理を書き下すのでなければdoは必要ありません。doとレイアウトルールによって、どこまでがコールバックネストが続いている範囲なのか指定されます。

その他の文法的な詳細は、Functionsの"Backcalls"節を参照して下さい。


チェインができない?

Backcallsを見た時は「へー攻めるなー」程度にしか考えていなかったのですが、ちょうどその時以下のようなコードを書いていたので、これはBackcallsで書くとしたらどうやって書くんだ?としばらく悩みました(three.jsとtween.jsのことはこの際措いておいて下さい)。


sample1.js

new TWEEN.Tween(camera.position)

.to({x: toX, y: toY, z: toZ}, 1000)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(function() {
camera.lookAt(target);
})
.onComplete(function() {
scene.add(newObject);
})
.start()

コールバックを受け取る関数同士のチェインをBackcallsで記述する方法は、ぱっと見では想像できません…


Cascades

ところで、LiveScriptにはCascadesという記法があり、主な用途としては、自分自身を返さない関数でもチェインのような記述の仕方をしたい場合のための記法のようです。どれだけインスタンス名を書きたくないんだよという気もしますが、多少気持ちが分かる時もあります。

例えば、three.jsが提供するミューテーター的な振る舞いをするAPIは基本的に自分自身を返したりはしないのでチェインして記述することはできませんが、LiveScriptであればこのようにつなげて(いるように)書くことができます。


sample2.ls

renderer = new THREE.WebGLRenderer antialias: true

..setSize 640, 480
..setClearColor 0x000000, 1
document.body.appendChild renderer.domElement


sample2.js

var x$, renderer;

x$ = renderer = new THREE.WebGLRenderer({
antialias: true
});
x$.setSize(640, 480);
x$.setClearColor(0x000000, 1);
document.body.appendChild(renderer.domElement);

単純に使う限り、「直前の、1レベル上のインデントの行を評価した結果が".."のアクセス対象になる」という感じでしょうか。

こちらも、その他の文法的な詳細は、Property Accessの"Cascades"節を参照して下さい。上で説明しているよりは、思ったより変態的です。


回答、あるいは断念

Backcallsについて悩みながらLiveScriptの記法をつらつら眺め、Cascadesを見つけた時にああこれを使えば一応記述はできそうかなーと思いました。以下のようになります。


sample3.ls

new TWEEN.Tween camera.position

..to x: toX, y: toY, z: toZ, 1000
..easing TWEEN.Easing.Quadratic.InOut
do
<-! ..onUpdate
camera.lookAt target
do
<-! ..onComplete
scene.add newObject
..start!


sample3.js

var x$;

x$ = new TWEEN.Tween(camera.position);
x$.to({
x: toX,
y: toY,
z: toZ
}, 1000);
x$.easing(TWEEN.Easing.Quadratic.InOut);
x$.onUpdate(function(){
camera.lookAt(target);
});
x$.onComplete(function(){
scene.add(newObject);
});
x$.start();

…これ、通常の記法より分かりやすいのでしょうか?例が悪い、というかユースケースが違うような気もします。ただ、記法に対する解決としてはこんなこともできなくはない、といったところでしょうか。

このような、比較的単純なコールバックであったり複数のコールバックをネストしたりしない場合は、通常の記法を利用した方がいいのではないかと考えています。その場合、以下のようになるでしょう。


sample4.ls

new TWEEN.Tween camera.position

.to x: toX, y: toY, z: toZ, 1000
.easing TWEEN.Easing.Quadratic.InOut
.onUpdate !-> camera.lookAt target
.onComplete !-> scene.add newObject
.start!


そもそもコールバックをネストしない

また、そもそも現代を生きている私達には、コールバックをネストする理由の大半であろう「非同期処理の逐次処理」をフラットに実現できる、deferred/promise的なイディオム(のパイプ)という文明の利器があります。最初の例で言えば以下のようになるでしょう。


sample5.ls

$.get 'http://example.com/api/foo' .then (dataFoo) ->

alert JSON.stringify dataFoo
$.get 'http://example.com/api/bar', dataFoo
.then (dataBar) ->
alert JSON.stringify dataBar
$.get 'http://example.com/api/baz', dataBar
.then (dataBaz) !->
alert JSON.stringify dataBaz


sample5.js

$.get('http://example.com/api/foo').then(function(dataFoo){

alert(JSON.stringify(dataFoo));
return $.get('http://example.com/api/bar', dataFoo);
}).then(function(dataBar){
alert(JSON.stringify(dataBar));
return $.get('http://example.com/api/baz', dataBar);
}).then(function(dataBaz){
alert(JSON.stringify(dataBaz));
});

そして、deferred/promiseが提供するのは上記のような記法上のメリットだけではなく、全体的に適用することで非同期処理を柔軟/簡潔に扱えるようになるといった設計のレイヤーでのメリットがあります。Backcallsを利用するとしたら、ちょっとした使い捨ての非同期処理の記述に留めるのが無難かなという結論です。


LiveScript自体の評価

記法リストを上から下まで眺めると分かりますが、LiveScriptは記法上の省力化のための(ともすればアドホックとも言える)ギミックがかなり盛り込まれている言語です。このような言語は、エンジニア毎に書き方が大幅に変わってくる、あるいは技巧的なレベルに大きな差がでやすく、そもそも半年後の自分が今書いているコードを即座に読み下せるかという保守性の懸念があります。

同程度の知見のエンジニアを揃える困難さや保守性の問題から、大規模開発に投入するには少し躊躇する印象です。一方で、非常に軽快にコーディングができるところは大変気に入っているので、まずはサンデープログラムやちょっとしたユーティリティの実践に導入してみようと考えています。