標高+1m

Don't be rational.

instaparseが気に入った話とメッセージ送信の記法が関数型と意外と相容れる話

 EBNFでパーサを書けるinstaparseがとても気に入りました。パーサコンビネータとかをよく使っていたので最初は、文字列でパーサ書くなんて楽しくないじゃん!て思ってましたが、実際に使ってみたら楽ちんすぎて虜になりました。EBNFどころかBNFもうろ覚えだったのですが、instaparseでパーサを作成 - Developers.IOとかinstaparseseでCSVパーサーを書いてみる - Code Aquariumとかを参考にさせてもらって見よう見まねでそれっぽいものが書けました。今はなんでもパースしたい気分です。

 で、なににinstaparseを使ったのかという話です。おとといあたりに去年の記事を見返してたら

Smalltalkの構文は書いてると自然に笑みがこぼれるほど好き

って書いてあるのを見つけて、そういえばそうだったなと思い出したんですね。#() inject: X into: Fとかですね、ほぼ英語じゃないですか。コードがスラスラ読めるんですよ。

 ここでふと、この恰好良いメッセージ送信記法をオブジェクト指向から切り離したらどうなるのかな、と気になったんです。メッセージ送信の形をしてるけど、ただの関数呼び出しに置き換えちゃうんです。

map :: [a] * (a -> b) -> [b]
VEC map: FN =
  (VEC fst FN) consOnto: (VEC rest map: FN)

わりかし悪くないんじゃないかと思って、実際に触ってみたくて昨日の夜に汗でべとべとになりながらinstaparseで遊んでみたというわけです。

こういう入力から

VEC `map` FN =
  (VEC rest `map` FN) conj: (VEC first FN)


FOO goodNumber? =
  FOO `<` 5 ifTrue: true ifFalse: false


BAR =
  [X 1, Y 2] let: X `*` Y


N snakeOfAge =
  { :name :sam :age N }


SNAKE birthday =
  SNAKE associate: :age to: (SNAKE `get` :age) `+` 1


APP main: ARGS =
  [1 2 3 4.89 5] `map`    sqrt
                 `filter` goodNumber?
                 `map`    snakeOfAge
                 `map`    birthday

こういう出力が吐ける

((defn map
  [VEC FN]
  (conj (map (rest VEC) FN) (FN (first VEC))))
 
 (defn goodNumber? [FOO] (ifTrueifFalse (< FOO 5) true false))

 (def BAR (let [X 1 Y 2] (* X Y)))

 (defn snakeOfAge [N] {:age N, :name :sam})
 
 (defn birthday
  [SNAKE]
  (associateto SNAKE :age (+ (get SNAKE :age) 1)))
 
 (defn main
  [APP ARGS]
  (map (map (filter (map [1 2 3 4.89 5] sqrt) goodNumber?) snakeOfAge) birthday)))

GISTはここ

S式は、せっかくパースした物を文字列にチマチマ戻さずに済むので遊び翻訳のターゲットとして最高ですね。

 これを遊び以上の物にしようとはあんまり考えてないんですが、もしかしたら面白い使い道があるかもしれないなあ。

 なにはともあれ、instaparseがとっても素敵なのでパーサをパッと書きたいときは(:require [instaparse.core :as insta])すると良いのではないでしょうか。