CoffeeScriptの{}
Hoge = require('Hoge').Hoge
↓
var Hoge; Hoge = require('Hoge').Hoge;
CoffeeScriptで{}を使用することで上記コードを短縮表記できることに気づいた。
{Hoge} = require('Hoge')
↓
var Hoge; Hoge = require('Hoge').Hoge;
生成されるJavascriptは等価だ。なるほど。
CoffeeScriptを使うべきか、使わざるべきか?
最近CoffeeScript界隈のブロゴスフィア(死語)を賑わせていた「CoffeeScriptを使うべきか、使わざるべきか?」という話題についてまとめてみた。
以下の記事紹介は超訳かつ要約なので詳しく知りたい人は元記事を参照のこと。
ことの発端はこの記事。
SnackJSの作者がCoffeeScriptをディスる。
A Case Against Using CoffeeScript by Ryan Florence
デバッグの問題
CoffeeScriptが生成するJavaScriptはきちんとしているけど、結局は自分が書いたコードじゃないため読みにくい。自分で直接書いたほうが見やすい。
それにCoffeeScriptをデバッグするワークフローは大変だ。
- まず問題がJavaScript内のどこで発生したのかを突き止める(CoffeeScriptのコードと行単位で対応してないから大変だ)
- そして、そのJavaScriptを理解する(自分が書いたコードじゃないからね)
- 理解した後、問題の原因を探す
- 問題の原因を把握した後に、直すべきCoffeeScript内のコードを探す
- CoffeeScriptを修正する
- コンパイルする(大抵の場合は自動化されてるけど、それでもステップの1つだ)
- 問題が解決した場合はそれでよし、しかしそうでない場合は・・・
- コンパイルが上手く行ったのか疑う
- 自分の予想通りになっているかJavaScriptを確認する(スタートに戻る)
普通にJavaScriptを書いてデバッグする場合のワークフローはこうなる。
- JavaScript内の問題を見つける(自分が書いたコードの!)
- 修正する
- 問題が解決した場合はそれでよし、しかしそうでない場合はスタートに戻る
どう考えても自分で書いたJavaScriptをデバッグしたほうがいい。
シンタックスがみにくい
if (five && six && seven) doStuff();
doSomething() if five and six and seven人間は言葉よりもシンボルを認識しやすいものだ。
そのため、&&のほうが何を意味しているのか理解しやすい。
ワンライナーは魅力的だけど、恐ろしい
CoffeeScriptはロジカル文を書くよりも一文でコードを書くことを推奨している。
eat food for food in foods一見よさそうに見えるが、こんな風に書くとどうしようもなくなる。
wash plate, brush, sink for key, plate of dishes when plate.dirty if meal.status is 'done'意味不明だ。
CoffeeScriptは"bad parts"を持っている
一見美しい言語に見えるけども、すべてがそうではない。
これは奇妙に思える。
getUser = (id) -> url = "users/#{id}" dfd = $.ajax url: url format: 'json' method: 'get' url: url promise: dfd.promise()$.ajaxを代入しているのではなく実行しているのだと把握するのは難しい。それにもしCoffeeScriptを知らない人は最後の2行に困惑するだろう。
もっと自分の意図通りに書くとこうなる。
getUser = (id) -> url = "users/#{id}" dfd = $.ajax url: url format: 'json' method: 'post' return url: url promise: dfd.promise()しかしこれはCoffeeScriptがカバーしようとしている"bad parts"であるautomatic semi-colon insertion(ASI)でパースエラーになる。
return { foo: 'bar', baz: 'qux' } // not returned due to ASIファットアロウ(Fat arrow)はアンチパターンだ
ファットアロウはクールに見える。jQueryを使ったコード:
var widget = { attach: function () { this.el.bind('click', $.proxy(function (event) { doStuffWithThis(); }, this)); } };がこうなる。
widget = attach: -> el.bind 'click', (event) => doStuffWithThis()無名コールバック関数を連鎖させていくのはアンチパターンの1つで、恐ろしいホワイトスペース問題を引き起こす。ファットアロウは忘却へいたる道を指し示している。
しかし、CoffeeScriptでこう書くこともできる。widget = attach: -> el.bind 'click', $.proxy this, 'handler' handler: (event) -> doStuffWithThis()重要なホワイトスペース + スパゲッティ === 死
重要なホワイトスペースはとってもクールだ。
しかし、30行のぐちゃぐちゃにネストしている無名コールバック関数や終わらないjQueryのメソッドチェイン、いい加減な条件文(if else if if elseif else if unless else)みたいな酷いJavaScriptで書く人がCoffeeScriptを書いたらもっとひどい事に成るだろう。
CoffeeScriptは美しい、けれども使うな
CoffeeScriptを書いているときは楽しいけれど、いずれ悪夢に変わるだろう。理解すること、デバッグすることが難しくなるからだ。
結局これは我々の現状と同じなんだけれども。
上記記事に対する反論記事。
CoffeeScript is not a language worth learning(CoffeeScriptは学ぶに値する言語ではない) by Reg Braithwaite
A Case Against Using CoffeeScriptの中で、Ryan Florenceは下手くそなCoffeeScriptを書く人のプログラムはJavaScriptで書いた場合よりもひどくなると語っている。率直に言って彼は正しい。この世の中には恐ろしい出来の生成されたJavaScriptコードがある。スタージョンが言ったように「あらゆるものの90%はクズである」。しかし、90%のクズがクズである理由はCoffeeScriptにあるのだろうか?私はそうは思わない。
CoffeeScriptはJavaScriptだ
CoffeeScriptは学ぶに値する言語ではない。なぜならCoffeeScriptは言語ではないからだ。CoffeeScriptはJavaScriptだ。"CoffeeScriptで考える"のではなく、"JavaScriptで考える"のだ。
明らかに、CoffeeScriptは異なるシンタックスを持っている。しかし、それは極めて表面的なものだ。JavaScriptが英語だとしたら、CoffeeScriptはフランス語のような他の言語でも、ジャマイカン・パトワのような方言でもない。テクニカルなジャーゴンみたいなものだ。
CoffeeScriptはなプログラムを書く上での劇的な新しい方法(モナドのような)をもたらしてくれるものではない。すべての変換はローカルなものだ。CoffeeScriptのスニペットを見てもらえば、それが他の部分にはなんの影響もないJavaScriptに変換されることがわかってもらえるだろう。
this.render()の代わりに@render()を書くとしよう。これはただの短縮記法で、言語じゃない。こう書くと
if foo and @get('bar') doThis() doThat()こうなる。
if (foo && this.get('bar')) { doThis(); doThat(); }これのどこに躍起になるんだ?CoffeeScriptは配列内包表記や破壊的代入、罵られた"ファットアロウ"など沢山の器用な変換を行う。これらはすべてJavaScriptではできないけどCoffeeScriptだからできる"言語機能"じゃない。
私はこれらの機能を別の視点で見る。これらはJavaScriptのデザインパターンだ。CoffeeScriptを謎めいたJavaScriptにコンパイルするための言語ではなく、スタンダードなデザインパターンにそったコードを生成してくれるJavaScriptだと考えている。これがループを書く方法だ。これがデフォルト引数をとる関数を書く方法だ。これがthisを指す定値を持つ関数を書く方法だ。これがクラス志向OOを書く方法だ。これがsuper()を関数内で呼ぶ方法だ。
CoffeeScriptが生成するJavaScriptは一貫性を持ち、JavaScriptが持つ一般的な問題を標準的な方法で解決する方法を持っている。一番素晴らしいのは、CoffeeScriptは同じ問題を同じ方法で正確に解決してくれることだ。
想像してもらいたい。Javaを使う人々がJavaScriptを書いたらどうなるか。彼らはデザインパターンを利用するだろう。彼らは自動的にデザインパターンスケルトンを生成してくれるIDEを使うだろう。
class OneTimeWrapper constructor: (@what) -> K: (fn, args...) -> functionalize(fn)(@what, args...) @what T: (fn, args...) -> functionalize(fn)(@what, args...) chain: -> new MonadicWrapper(@what) value: -> @whatIDEは自動的にこれを拡張する。
OneTimeWrapper = (function() { function OneTimeWrapper(what) { this.what = what; } OneTimeWrapper.prototype.K = function() { var args, fn; fn = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; functionalize(fn).apply(null, [this.what].concat(__slice.call(args))); return this.what; }; OneTimeWrapper.prototype.T = function() { var args, fn; fn = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; return functionalize(fn).apply(null, [this.what].concat(__slice.call(args))); }; OneTimeWrapper.prototype.chain = function() { return new MonadicWrapper(this.what); }; OneTimeWrapper.prototype.value = function() { return this.what; }; return OneTimeWrapper; })();謎めいたところはない。これは私が書くべきJavaScriptだ。
I treat CoffeeScript as a big TextMate snippet that's always editable.―@topfunky
私にとっては、CoffeeScriptを書くことはJavaScriptを書くことだ。省略記法でスタンダードなデザインパターンを意味することを補助してくれるツールがあるだけだ。
CoffeeScriptは言語ではない。JavaScriptのためのコーディングスタンダードだ。
私見をまとめよう。"CoffeeScript"は新しいプログラミング言語ではない。CoffeeScriptはJavaScriptをスタンダードなデザインパターンで書くための省略形の集合だ。生成されるJavaScriptは高度に最適化されたスパゲッティではなく、JavaScriptのGood Partsだ。
CoffeeScriptは他人が書いたコードを読みやすくしてくれる。CoffeeScriptユーザーは同じ方法でループを書く。同じ方法でクラスを書く。私たちは同じパターンを使う。なぜならCoffeeScriptがそれらを生成するからだ。
これはPythonの重要なホワイトスペースと同じ事だ。それぞれが少しずつ違ったやり方でコードを書くことは損失だ。スタンダードなインデンテーションで、スタンダードなOOPで、スタンダードなループで書くことでJavaScriptは読みやすく、理解しやすく、保守しやすいものになる。
感想
僕は後者の記事を支持する。
Ryan Florenceのデバッグをする過程が面倒だという指摘はもっともだが、Reg Braithwaiteが言うようにCoffeeScriptでJavaScriptを書くことによってコードが平準化されてデバック自体はしやすく成るのではないかと思う。
それにCoffeeScriptはJavaScriptのGood Partsの集合体だという指摘もごもっともで、僕はCoffeeScriptを使うことでよりJavaScriptの理解が深まったと思う。
というわけで、CoffeeScriptを使おう!
クライアントサイドフレームワークbrunchでアプリを作ってみた
http://ninoseki.github.com/local-scrum/
こんな感じのスクラムボード風HTML5アプリをクライアントサイドフレームワークbrunchを使って作ってみたのでメモ。
以下はbrunchの紹介。
brunchって?
brunchは軽量なクライアントサイドフレームワーク。
Backbone.js(+Underscore.js),Stich,Eco,Stylus,jQueryといった各種フレームワーク&ライブラリの組合せから成るフレームワークで、CoffeeScriptを使用することが前提。
基本はBackbone.js。
普通にBackbone.jsを使うのと何が違うの?
brunchを使うとView周りがきれいに書けるところが良いと思う。
この例のようにHTMLの中にテンプレートを記述して使用するやり方だとごちゃごちゃしてしまいがち。
brunchはecoを使うことで、ここら辺がきれいに分離できる。
あと、ディレクトリ構成がかっちり決まるところもナイス。
インストール
npm install -g brunch
プロジェクト作成
brunch new [
]
brunchプロジェクトが作成される。
プロジェクトのディレクトリ構成は以下のようになる。
build/ web/ config.yaml src/ app/ collections/ (Backbone.jsのCollection) models/ (Backbone.jsのModel) routers/ (Backbone.jsのRouter) styles/ (Stylus) templates/(Eco) views/ (Backbone.jsのView) vendor/ (サードパーティのライブラリ)
src/app以下にBackbone.jsを基本にしてコードを書いていくことになる。
brunch watch [
]
でコードの変更を監視して自動的にコンパイルが行われる。
Stylusはbuild/web/main.cssに、Backbone.jsとEcoはbuild/web/js/main.jsにまとめてコンパイルされる。
まとめ
Backbone.js + CoffeeScriptをベースに効率的かつ一貫性のある開発ができるフレームワーク、それがbrunch。
これは中々いけてるんじゃないでしょうか。
公式で公開されているサンプルコード(Backbone.jsのTODOアプリのbrunch版)を見てもらえば、brunchの良さがわかってもらえるはず。
check it out!
Instagram Engineering Challengeを解いてみる
http://instagram-engineering.tumblr.com/post/12651721845/instagram-engineering-challenge-the-unshredder
以上のようにバラバラに切断された画象を以下のように戻せ、という問題。
画象の分割数は予め与えられれているものとする。(自動的に画象の分割数を求めるとボーナス得点有り)
とりあえず隣接するカラムの類似度(画素間の距離)を行単位で求め、類似度が高いものから順に並べていけばいいんじゃないかと考えた。
画素間の距離をRGBで求めるとうまく行かなかったけど、HSVで求めるとうまく行った。ふむ。
# -- coding utf-8 -- from PIL import Image, ImageChops, ImageStat import math, operator, sys, colorsys def calc_diff(p1, p2): p1 = colorsys.rgb_to_hsv(p1[0] / 255., p1[1] / 255., p1[2] / 255.) p2 = colorsys.rgb_to_hsv(p2[0] / 255., p2[1] / 255., p2[2] / 255.) return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2 + (p1[2] - p2[2]) ** 2) def find_match_scores(image, width, height, columns, shred_width): # 行単位での画素の距離の総和をもとめる # 値が大きいほど類似度が低い scores = {} for i in range(columns): right1 = shred_width * (i + 1) - 1 left1 = shred_width * i next_left = None score = None for j in range(columns): if i == j: continue right2 = shred_width * (j + 1) - 1 left2 = shred_width * j tmp_score = 0.0 for h in range(height): p1 = image.getpixel((right1, h)) p2 = image.getpixel((left2, h)) tmp_score += calc_diff(p1, p2) if score is None or score > tmp_score: score = tmp_score next_left = left2 scores[left1] = (next_left, score) return scores def find_rightest_colum(image, width, height, scores): right = None # 遷移先が重複しているものがある場合、スコアが高いほうが右はじのカラムだと仮定する for k, v in scores.items(): for k2, v2 in scores.items(): if k == k2: continue if v[0] == v2[0]: if v[1] > v2[1]: right = k else: right = k2 return right # 上の仮定に該当するものがない場合はスコアが一番たかいものを右はじのカラムだと仮定する if right is None: max_item = sorted(scores.items(), key = lambda x : x[1])[0] return max_item[0] def find_order(rightest_column, scores): order = [] # 右はじのカラムにたどりくまでを逆算してもとめるため、key-valueの関係を逆転させる reverse_scores = {} for k, v in scores.items(): reverse_scores[v[0]] = k current = rightest_column while True: order.append(current) if current not in reverse_scores: break current = reverse_scores[current] # 左から右の順に変更 order.reverse() return order def paint_out(image, width, height, shred_width, order): output = Image.new('RGBA', (width, height)) for i in range(len(order)): source_box = (order[i], 0, order[i] + shred_width, height) column = image.crop(source_box) destination_box = (i * shred_width, 0, (i + 1) * shred_width, height) output.paste(column, destination_box) return output if __name__ == '__main__': image = Image.open("TokyoPanoramaShredded.png") width, height = image.size columns = 20 shred_width = shred_width = width / columns # 各カラム間の類似度を計算する scores = find_match_scores(image, width, height, columns, shred_width) # 右はじのカラムを求める rightest_column = find_rightest_colum(image, width, height, scores) del(scores[rightest_column]) # 並び順を求める order = find_order(rightest_column, scores) output_image = paint_out(image, width, height, shred_width, order) output_image.save("TokyoPanoramaUnshredded.png")
Backbone.jsでCollectionを初期化する方法
なんらかの条件でもってCollectionにひもづいてるModelを削除しようとしてつまった。
Collection.prototype.destroyAll = function() { this.each(function(model) { if (condition) { model.destroy(); } } }
これは動作しない。
Model.destroy()は自分自身を削除してしまうので、イテレーターもそこで止まってしまうわけだ。
以下のように一旦Collection.modelsの複製を作って、それをイテレートしながら削除すれば正常に動作する。
Collection.prototype.destroyAll = function() { var cloneModels = _.clone(this.models); _.each(cloneModels, function(model) { if (condition) { model.destroy(); } }); }
禅の公安スタイルでCoffeeScriptを学ぶ「coffeescript-koans」
https://github.com/sleepyfox/coffeescript-koans
禅において弟子に与えられる課題に公安というものがある。(「隻手の声」とか)
それが転じて、プログラミングの世界では穴埋め形式でプログラミング言語を学ぶことがKoan(Koans)と呼ばれるようになった。(ex.ruby_koans)
今回紹介するcoffeescript-koansはCoffeeScriptを公安スタイルで学習できるものだ。
ひと通りCoffeeScriptのチュートリアルを読んで文法を学んだ後でチャレンジしてみよう。
こういう場合はどうなるんだっけ?という疑問がでてきて、より深くCoffeeScriptを学べるはずだ。
実行方法
テストランナーはJasmineが使用されている。
基本的には/koans以下にある.coffeeファイルにあるFILL_ME_INに答えを埋めていくことになる。
例.
describe 'About Arrays', -> it 'should understand range slicing', -> oneToTen = [1..10] expect(oneToTen[3..5]).toEqual(FILL_ME_IN) myString = "my string"[0..2] expect(myString).toEqual(FILL_ME_IN) firstTwo = ['one', 'two', 'three'] expect(firstTwo[FILL_ME_IN]).toEqual(['one', 'two'])
答えを記述した後は、答えを記述した.coffeeファイルを直接コンパイルするかまたはcakeコマンドでビルドするしよう。
cake build
その後、KoansRunner.htmlをブラウザ上で実行することで回答を確認することができる。
まとめ
coffeescript-koansでCoffeeScriptの基礎から応用までを公安スタイルで楽しく学ぶことができる。
また、同時にJavascript用のBDDフレームワークであるJasmineについても学ぶことができて一石二鳥だ。
ちょっとCoffeeScriptの文法をかじってみた人は、チャレンジしてみてはいかがだろうか。