音の鳴るブログ

鳴らないこともある

CoffeeScriptの演算子を拡張する

以下、全編 CoffeeScript でお届けします。

# CoffeeScriptの演算子を拡張する

# できないんだけど, できたら便利なんで無理やりやってみる.


# こうなってしまうのを
[ 1, 2, 3 ] + 100 # => "1,2,3100"

# このようにしたい
[ 1, 2, 3 ] + 100 # => [ 101, 102, 103 ]

# こうなってしまうのを
[ 1, 2, 3 ] * 100 # => NaN

# このようにしたい
[ 1, 2, 3 ] * 100 # => [ 100, 200, 300 ]


# JavaScript で出来ないことは CoffeeScript でも出来ないのだけど,
# プロトタイプの拡張とコンパイラの字句解析をハックしたらできるんじゃないかと思った.


# プロトタイプを拡張して
Array::__add__ = (b)-> @map (a)-> a + b
Array::__mul__ = (b)-> @map (a)-> a * b

# これを
[ 1, 2, 3 ] + 100 # => "1,2,3100"

# こうする
[ 1, 2, 3 ].__add__(100) # [ 101, 102, 103 ]

# これを
[ 1, 2, 3 ] * 100 # => NaN

# こうする
[ 1, 2, 3 ].__mul__(100) # => [ 100, 200, 300 ]


# なるほど, いけそう
# CoffeeScriptはこんな感じで段階的にコンパイルできるので
tokens = CoffeeScript.tokens("[1, 2] + 3")
nodes  = CoffeeScript.nodes(tokens)
code   = nodes.compile(bare:true)


# 構文木(nodes)にする前にトークンを操作する
# 3番目の要素には位置情報が入っているけど空のオブジェクトを入れておけば OK
tokens = [
  # TAG           VALUE      POSITION
  [ "["         , "["      , {} ]
  [ "NUMBER"    , "1"      , {} ]
  [ ","         , ","      , {} ]
  [ "NUMBER"    , "2"      , {} ]
  [ "]"         , "]"      , {} ]
  [ "+"         , "+"      , {} ] # <= - これを削除して
  [ "."         , "."      , {} ] # <= + かわりに挿入する
  [ "IDENTIFIER", "__add__", {} ] # <= +
  [ "CALL_START", "("      , {} ] # <= +
  [ "NUMBER"    , "3"      , {} ]
  [ "CALL_END"  , ")"      , {} ] # <= + TERMINATORの前とか
  [ "TERMINATOR", 0xa      , {} ] #      適切な場所でちゃんと閉じる
]


# 具体的には以下のような手順
# CALL_START に対応する CALL_END の位置をスタックで管理する

# - 算術演算子がきたら気合で . __add__ ( などトークンを挿入する
# - あと CALL をスタックに追加
# - 途中で ( [ { などが出てきたら スタックに BRACKET を追加
# - 途中で ) ] } などが出てきたら スタックから BRACKET を削除
# - いけそうだったら CALL_END を挿入して, スタックから CALL を削除
# - カンマや TERMINATOR, INDENT, OUTDENT タグの直前でも CALL_END くる

# デメリットとして算術演算子が全部関数呼び出しになるので
# パフォーマンスは当然悪くなります.

"以上です"