coffeescriptでメタプログラミング

coffeescriptメタプログラミングをしようとしてハマったので、メモ。

最初に思いついたコード

最初、メタプログラミングしようとして下記のようなコードを書いた。

class Hoge
  i_methods =
    foo:
      message: 'foo'
    bar:
      message: 'bar'

  for i of i_methods
    @::[i] = ->
      console.log(i_methods[i].message)

  c_methods =
    fiz:
      message: "fiz"
    piyo:
      message: "piyo"

  for c of c_methods
    @[c] = ->
      console.log(c_methods[c].message)

console.log('----')
new Hoge().foo()
new Hoge().bar()
console.log('----')
Hoge.fiz()
Hoge.piyo()

実行結果

----
bar
bar
----
piyo
piyo 

あれ・・・意図したのと結果が違う。

この時、出力されたJavaScriptはこんな感じ。

変換されたJavaScript

var Hoge;

Hoge = (function() {
  var c, c_methods, i, i_methods;

  function Hoge() {}

  i_methods = {
    foo: {
      message: 'foo'
    },
    bar: {
      message: 'bar'
    }
  };

  for (i in i_methods) {
    Hoge.prototype[i] = function() {
      return console.log(i_methods[i].message);
    };
  }

  c_methods = {
    fiz: {
      message: "fiz"
    },
    piyo: {
      message: "piyo"
    }
  };

  for (c in c_methods) {
    Hoge[c] = function() {
      return console.log(c_methods[c].message);
    };
  }

  return Hoge;

})();

console.log('----');

new Hoge().foo();

new Hoge().bar();

console.log('----');

Hoge.fiz();

Hoge.piyo();

ループで同じ変数が上書きされているのが原因っぽい。

という訳で、修正した。

class Hoge
  @template: (obj) ->
    ->
      console.log(obj.message)

  i_methods =
    foo:
      message: 'foo'
    bar:
      message: 'bar'

  for i of i_methods
    @::[i] = @template(i_methods[i])

  c_methods =
    fiz:
      message: "fiz"
    piyo:
      message: "piyo"

  for c of c_methods
    @[c] = @template(c_methods[c])

console.log('----')
new Hoge().foo()
new Hoge().bar()
console.log('----')
Hoge.fiz()
Hoge.piyo()

コードを読めば分かるけど、関数を返す関数を定義しておいて、ループ内で呼んでいる。

実行結果

----
foo
bar
----
fiz
piyo 

これで意図した動きになった!