みどりねこ日記

よくわからないけど、頑張りますよ。

Pythonを呼べるLispを作った話

この記事はLisp Advent Calendar 2016 5日目の記事です。

Futhonとは

Futhonとは、Pythonが呼べるLispである。 先日作ってみた。 読み方は「ふとん」である。

本記事は、Futhonの内部実装がどうなっているかを簡単に説明する。 すごく小さな実装(ソースコードすべて含んで500行くらい)なので、この記事とコードを合わせて読めば誰でもちょっとしたおもちゃ言語が作れるようになるんじゃないかと思う。

作った動機

最近周りで機械学習がアツい。

Pythonディープラーニング機械学習ライブラリが充実していて、モデルを簡単に作ったり試したりすることができる素晴らしい環境になっている。

一方で、Lispニューラルネットを組もうと思ってもライブラリがなかったり、あってもドキュメントが少なかったりしたのでどうしたものかと悩んでいた。

そこで、ClojureJavaのリソースを利用できるように、Pythonのライブラリを自在に呼び出せるLispを実装して解決しようという考えに至った。 その結果完成した言語がこちらのFuthon言語。 Clojureにはcore.matrixとかあるから自分でそういうライブラリ実装すればいいじゃないかという意見は聞こえない。たぶん言語実装したほうがコスト小さい…。

実装の方針

Pythonのライブラリを自作言語から呼ぶ方法はいくつかある。 そのうちで検討したのは以下の3つである。

まず一つ目は実装がしんどそうな割にあまりメリットもなく思われたので候補から外した。そもそもC++について詳しくないので難航するのは目に見えていた。 二つ目が最有力候補だったけれどREPLの実装の仕方がわからなかったために断念。 というのは、REPLを実装するには順に入力された式を評価しながら何らかの方法でPython側の環境を管理する必要があるが、その仕方が思いつかなかった。 実はPythonにはcode.InteractiveConsoleというクラスが用意されていて、これの継承クラスを書いてやれば簡単にできることをあとあと知った。 結局、最後のメタプログラミングをする方法が実装に手間も時間もかからなさそうだったのでこれを採用した。

実装の大まかな流れ

大きく分けると、インタプリタはだいたい以下の要素で構成されている。

  • トークナイザ
  • パーザ
  • 評価器
  • プリンタ

トークナイザとパーザは、ライブラリによっては一緒に行われる場合もある。 これらを順に実装していくことになる。ここでもその順番に説明していく。

データ型と文法

最初にプログラミング言語のデータ型と構文を決める。 Futhonには以下のデータ型をプリミティブとして持たせることにした。

  • Vector
  • Set
  • HashMap
  • Symbol
  • Keyword
  • Lambda

以下がBNFで表記した文法。

program ::= sexpr

sexpr ::= atom | sequence | quoted

atom ::= string | regex_string | symbol
sequence ::= vector | list | hashmap | set

quoted ::= '\'' sexpr

symbol ::= '[_@\w.\-><\+\-\*\/\?!:=]+'
string ::= '".*?(?<!\\)(\\\\)*?"'
regex_string ::= '\#' string

vector ::= '\[' sexpr* '\]'
list ::= '\(' sexpr* '\)'
hashmap ::= '{' (sexpr sexpr)* '}'
set: '\#{' sexpr* '}'

Futhonプリミティブであるデータ型はFuthonObjectを継承するクラスとして実装する。 そうすると独自の型なのかPython側のオブジェクトなのかがisinstance(x, FuthonObject)するだけで判別できる。 ついでに__repr__のように、あったら便利かもみたいなメソッドをデータ型ごとに追加していく。 これらの詳しい定義はdatatypes.pyを参照のこと。

class Symbol(FuthonObject):
    def __init__(self, name):
        self.name = name
        
    def __repr__(self):
        return self.name

    def __hash__(self):
        return hash(self.name)

    def __eq__(self, other):
        return isinstance(other, Symbol) and self.name == other.name

    def __ne__(self, other):
        return not self.__eq__(self, other)

文法

トークナイザ・パーザ

ソースコードからいきなり実行しようとすると、実装が難しい。 なので、ソースコードからトークンを切り出したり、木構造を生成して後に続く処理を楽にする。 これをするのがトークナイザとパーザ。 パーザの生成にはPlyPlusというパーザジェネレータを使う。 ドキュメントはあまりないがサンプルはあり、BNF表記でLRパーザが生成される点が素敵。 このライブラリを使えばトークナイザを作る必要もないので「とりあえずパーズしたい」という場合には便利だと思う。

start: sexpr;

@sexpr: atom | sequence | quoted;

@atom: string | regex_string | symbol;
@sequence: vector | list | hashmap | set;

quoted: '\'' sexpr;

symbol: '[_@\w.\-><\+\-\*\/\?!:=]+';
string: '".*?(?<!\\)(\\\\)*?"';
regex_string: '\#' string;

vector: '\[' sexpr* '\]';
list: '\(' sexpr* '\)';
hashmap: '{' (sexpr sexpr)* '}';
set: '\#{' sexpr* '}';

WS: '[ \t\n,]+' (%ignore) (%newline);

あとは生成されたLRパーザを呼び出してやって、それぞれ対応するデータ型に変換してやればパージングは終わり。 この辺の実装はfuthon_parser.pyに書かれている。

評価器

式の評価

大抵のプログラミング言語のパーズ結果は抽象構文木である。これはもちろんFuthonでも同様。 そういうわけで、得られた木構造をゴリゴリ評価していく。 Futhonは変数を持つ言語なので、評価する際には変数と値の対応を管理する環境という機構が必要になる。 さらにFuthonからPythonのメソッドなどを呼び出せるようにしたいのでPythonメタプログラミングが必要になる。

環境

環境は変数と値を対応づけるための構造。 Futhonの環境は階層的な構造を持っており、そうすることで変数の局所的な定義などができるようになる。 例えばFuthonでは定義された関数は新しい環境を持つことになるが、同時に生成された時点の環境を上位に持つことで、その時点での環境を関数内から参照できるようになる。

コード上の実装としては上の環境へのリンクを持つ、変数からFuthonObjectへのdict型という感じ。 environment.pyを読めばだいたいわかるはず。

Pythonメタプログラミング

FuthonではPythonのメソッド呼び出しやクラス生成を実行時に行いたいので、これらを実現する方法がホスト言語側に求められる。 幸いPythonにはいくつかそういったものが用意されていて、例えばgetattrを使えばPythonでクラスやメソッドをロードしたり呼び出したりできる。 getattrPythonの組み込み関数で、引数としてPythonのオブジェクトと属性名をあらわす文字列の二つを取り、そのオブジェクトの属性を返す。

class Hello():
    def __init__(self):
        self.hello = "nice to meet you!"

    def greeting(self):
        return self.hello

hi = Hello()
getattr(hi, "hello")
# => "nice to meet you!"
getattr(hi, "greeting")
# => <bound method Hello.greetings of <__main__.Hello object at 0x7febcaf46470>>

getattrで得られたオブジェクトがメソッドなのかはinspect.ismethod()で判別できる。 ただしこれだけではBuilt-in functionなのかどうかまでは分からないので、isinstance(o, types.BuiltinFunctionType)などでチェックする必要がある。 Futhonではメソッド呼び出しと一部のBuilt-in functionのみをサポートしている。

モジュールをインポートする場合はimportlib.import_moduleにモジュール名を渡してやればモジュールオブジェクトが返ってくる。

importlib.import_module("numpy")
# => <module 'numpy' from '/home/delihiros/.pyenv/versions/3.5-dev/lib/python3.5/site-packages/numpy/__init__.py'>

このへんのコードはdynamic.pyにまとめた。

こういうちょっとメタ的な仕様は他にも色々あって、例えばBuilt-in functionであるtypeに3つ引数を与えるとPythonのクラスを新しく生成できたりする。 興味ある人は公式ドキュメントを読んでほしい。 今回は後述する事情により途中で飽きた必要そうでもなかったので取り入れなかった。

以上で評価をするための部品はできた。

評価を行う

あとは実際に抽象構文木の評価を行う。 基本的には抽象構文木の根から葉に向かって深さ優先探索で評価をしていく。 このとき、与えられたノードの型によって評価の仕方を分ける。 例えばFuthonObjectではなくPython側のオブジェクトだったら、そのまま返す。Symbolだったら環境から対応する値を見つけてきて返す、など。 ただし、リストの場合はFuthonの式の可能性があるので特別な評価を行う。 これらの処理はevaluator.pyにある。

def eval(self, expr, env):
    if not datatypes.isFuthonObj(expr):
        if isinstance(expr, list):
            return self.eval_list(expr, env)
        return expr
    elif datatypes.isSymbol(expr):
        return env.get(expr)
    elif datatypes.isKeyword(expr):
        return expr
    elif datatypes.isVector(expr):
        return datatypes.Vector([self.eval(e, env) for e in expr])
    elif datatypes.isSet(expr):
        return datatypes.Set([self.eval(e, env) for e in expr])

リストの場合は、その先頭にあるオブジェクトの型と名前を見て、合致する条件に対応した処理を行う。 例えば、先頭がdefifのように特別な意味を持つSymbolの場合、環境に対して操作を行ったり条件分岐を行ったりする。 先頭が関数やメソッドの場合はそれらを適用する。

def eval_list(self, expr, env):
    if len(expr) == 0:
        return expr
    head = expr[0]
    if datatypes.isSymbol(head):
        if head.name == 'def':
            env.set(expr[1], self.eval(expr[2], env))
            return None
...
    elif datatypes.isLambda(head):
        args = [self.eval(v, env) for v in expr[1:]]
        return self.apply_function(head, args, env)
...

REPL

評価もできるようになったので、あとは外部からプログラムを読み込めるようにすれば終わり。 入力をパースして評価、という処理を繰り返すだけのREPLを実装した。 コードはrepl.pyにある。

(def np (import numpy))
(def chainer (import chainer))

(def l1 (chainer.links.Linear 4 3))
(def l2 (chainer.links.Linear 3 2))

(def my-forward
  (fn [x] (l2 (l1 x))))

(def x (.astype (np.array [[1 2 3 4]]) np.float32))

(.data (my-forward x))
; [[-1.02830815  0.6110245 ]]

これでひとまず大体の機能は備わったので、ChainerやTensorFlowのようなライブラリを使ってニューラルネットLispで書けるようになった。 やったね!!!!!!

オチ

Hyっていう言語があるんでそいつを使いましょう。 実装する前に知りたかった。事前のリサーチは大事。

どうでもいいこと

最初はPalisというオシャレな名前を付けて開発していたが、研究室のメンバーが「FudabaのPythonなんだからFuthonにしなよ」と言ったのでそのようになった。

参考

言語実装資料まとめ

instaparseで遊ぶ

この記事はClojure Advent Calendar 2013 - Qiita [キータ]19日目の記事です。

instaparse

本記事ではinstaparseというライブラリを使って遊んでみます。

instaparseはEBNFやABNFで記述された文脈自由文法から自動的にパーサを生成してくれます。 左再帰、右再帰、曖昧性など、いかなる文脈自由文法でも動作します。 生成されたパーサは文字列からenliveまたはhiccup風の木構造を生成します。

今回は型なしラムダ計算を題材にします。

型なしラムダ計算

型なしラムダ計算は、型がない(または型がひとつしかない)ラムダ計算です。

ラムダ計算はとてもシンプルで、すべての計算が関数定義と関数適用だけで実現可能です。

ラムダ計算の構文は以下の3つからなります。

t ::=        項
    x        変数
  | \x . t   ラムダ抽象
  | t t      関数適用

定義くらいはほしいだろう、ということで、今回作るパーサは以下のような拡張をした構文にします。

lang ::= (expr | def)+
def  ::= var '=' expr ';'*
expr ::= (var | app | abs)
     |  '(' app | abs ')' ';'*
var  ::= [a-zA-Z-]+
app  ::= expr expr
abs  ::= '\\' var '->' expr

さて、上の構文を用いてパーサを作ってみます。

(ns lambda.core
  (:require [instaparse.core :as insta]
            [clojure.core.match :as match]))

(def parser
  (insta/parser
    "lang = (expr | def)+
    def = <w> var <'='> expr <';'*> <w>
    <expr> = <w>
    (var | app | abs
         | <'('> (app | abs) <')'>) <w> <';'*>
    var = <w> #'[a-zA-Z-]+' <w>
    app = expr expr
    abs = <'\\\\'> var <arrow> expr
    w = #'[\\n\\s]*'
    arrow = '->'
    "))

簡単に書けました。

実行すると以下のような結果が得られます。

(parser "true = \\x -> \\y -> x;")
; => [:lang [:def [:var "true"] [:abs [:var "x"] [:abs [:var "y"] [:var "x"]]]]]

驚きのお手軽感です。

お分かりかとは思いますが、langやdefなどがキーとなった木(ベクタ)ができています。そして< >で囲まれたexprやwは出力結果にはありません。 < >はパース結果を隠す役割があります。他にも正規表現が使えたりと大変便利なライブラリです。

またパース結果をグラフ化することもできます。

(insta/visualize (parser (slurp "sample.lambda")))

f:id:paprikas:20131217004809p:plain

せっかくパーサができたので評価できるようにします。

実装にあたってはこの辺とかこの辺を参考にしました。

α変換...本来だと条件によって置換しなくてもいいこともある。フムフム〜〜〜。

面倒だし、全部置き換えたところで一緒なので全部置換することにします。

(gensym)でユニークな変数が降ってくるので、\x -> \y -> x yのような形のものを、\unique_symbol -> \y -> \unique_symbol yのように変換します。 そのようにすれば適用する式の中身と変数名がかぶりませんね。

HaskellOCamlみたいなパターンマッチがClojureにもあったらなーと思っていたら、clojure.core.matchがなかなか良さそうなので使ってみました。

(def env (atom {}))

(defn evaluate [expr]
  (match/match
    expr
    [:def v e]
    (do (swap! env conj {v e}) v)

    [:var v]
    (get @env [:var v] [:var v])

    [:app [:var v] n]
    [:app (get @env [:var v] [:var v]) n]

    [:app [:app m n'] n]
    [:app (evaluate [:app m n']) n]

    [:app [:abs [:var v] body] n]
    (clojure.walk/postwalk
      (fn [z] (if (= z [:var v]) n z))
      (evaluate body))

    [:abs [:var v] body]
    (let [v' (str (gensym))]
      [:abs [:var v']
            (clojure.walk/postwalk 
              (fn [z] (if (= z [:var v]) [:var v'] z))
              (evaluate body))])))

(defn show [expr]
  (match/match
    expr
    [:lang & n]
    (reduce str (map show n))
    [:def v e]
    (str (show v) " = " (show e))
    [:var v]
    v
    [:app f x]
    (str "(" (show f) " " (show x) ")")
    [:abs arg e]
    (str "^" (show arg) "." (show e))))

(defn evaluate-all [expr]
  (match/match
    expr
    [:app m n]
    (evaluate-all (evaluate expr))
    :else expr))

(let [code
      (str
        "true = \\x -> \\y -> x;"                ;; "true = ^x.^y.x"
        "false = \\x -> \\y -> y;"               ;; "false = ^x.^y.y"
        "if = \\p -> \\t -> \\f -> ((p t) f);"   ;; "if = ^p.^t.^f.((p t) f)"
        "cons = \\x -> \\y -> \\f -> ((f x) y);" ;; "cons = ^x.^y.^f.((f x) y)"
        "car = \\p -> p true;"                   ;; "car = ^p.(p true)"
        "cdr = \\p -> p false;"                  ;; "cdr = ^p.(p false)"
        "(car ((cons true) false))"              ;; "^x.^y.x"
        "(((if true) false) true)")              ;; "^x.^y.y"
      parsed (parser code)
      results (map evaluate-all (rest parsed))]
 (map show results))

Clojureすごい!instaparseすごい!

というわけで、19日目はこれでおしまいです。

パーサの連接とか

前回までに出てきた基本的なパーサをくっつけることで複雑なパーサが書けるわけですが、 今回はそのような連接とかを手助けしてくれるパーサコンビネータを紹介?します。

Kern のパーサコンビネータはシーケンス化、反復、選択、それと括弧の対応とかの面倒を見てくれるみたいです。

例のごとく kern をロード。

(use 'blancas.kern.core)

skip-ws は入力のはじめにあるすべての空白をスキップ。

doc.core> (run (skip-ws letter) "\t \n x")
\x
nil

<|> はあたえられたパーサを成功するまで試す。すべてのパーサが失敗するか、入力を消費するパーサが失敗すると全体として失敗する。

doc.core> (run (<|> digit letter) "x")
\x
nil
doc.core> (run (<|> digit letter) "9")
\9
nil
doc.core> (run (<|> digit letter tab) "?")
line 1 column 1
unexpected \?
expecting digit, letter or tab
nil

で、 >> は1つ以上のパーサを適用する。最後のパーサの結果だけを返す。

doc.core> (run (>> digit digit letter) "91x")
\x
nil

<< は1つ以上のパーサを適用する。最初のパーサの結果だけを返す。

doc.core> (run (<< upper (sym* \-) digit digit) "F-16")
\F
nil

<$> は与えられたパーサを入力に適用して、そのあとパースした結果に対して与えられた関数を適用、その結果を返す。

doc.core> (run (<$> #(* % %) dec-num) "12")
144
nil

<*> は2つ以上のパーサを適用して、それらの結果をベクタにして返す。

doc.core> (run (<*> dec-num tab oct-num tab hex-num) "737\t747\tA340")
[737 \tab 487 \tab 41792]
nil

<:> はバックトラックするパーサらしい。もし失敗しても消費した入力は回復。

doc.core> (:input (parse (<:> float-num) "5005.a"))
(\5 \0 \0 \5 \. \a)

many は入力に対しパーサを0回以上、失敗するまで適用。many1 は1回以上で、最低1回生功しないと失敗。

doc.core> (run (many letter) "123")
[]
nil
doc.core> (run (many letter) "xyz")
[\x \y \z]
nil
doc.core> (run (many1 letter) "xyz")
[\x \y \z]
nil
doc.core> (run (many1 letter) "123")
line 1 column 1
unexpected \1
expecting letter
nil

optional はパーサが成功するか消費を行わかなった場合に全体として成功する。

doc.core> (run (<+> (optional (sym* \-)) dec-num) "-45")
"-45"
nil
doc.core> (run (<+> (optional (sym* \-)) dec-num) "45") 
"45"
nil
doc.core> (run (<+> (optional float-num) letter) "45.A")
line 1 column 4
unexpected \A
expecting digit
nil

option は与えられたパーサを適用する。もし入力を消費せずに失敗したら、与えられた値を結果として返すっぽい。

doc.core> (run (option 3.1416 float-num) "foo")
3.1416
nil

skip は1つ以上のパーサを取ってそれらの結果をすべて飛ばす。結果として nil を返す。

doc.core> (run (skip letter digit letter digit) "A5E8") 
nil
nil

skip-many はパーサを0回以上適用し結果を飛ばす。nil を返す。skip-many は1回以上。

doc.core> (run (skip-many letter) "xyz")
nil
nil
doc.core> (run (skip-many1 letter) "xyz") 
nil
nil
doc.core> (run (skip-many1 letter) "123")
line 1 column 1
unexpected \1
expecting letter
nil

sep-by は与えられたパーサによって分けたものに対して別のパーサを0回以上適用する。sep-by1 は1回以上。

doc.core> (run (sep-by (sym* \&) (many1 letter)) "a&bb&ccc&ddd")
[[\a] [\b \b] [\c \c \c] [\d \d \d]]
nil

end-by は与えられたパーサによって別れる/終わるものに対して別のパーサを0回以上適用する。end-by1 は1回以上。

doc.core> (run (end-by (sym* \&) (many1 letter)) "a&bb&ccc&")
[[\a] [\b \b] [\c \c \c]]
nil

between は1,2つ目のパーサの間のちに対して3つ目のパーサを適用する。

doc.core> (run (between (sym \() (sym \)) dec-num) "(1984)")
1984
nil

<+> はひとつ以上のパーサをとる。で、結果を潰してくっつけて、文字列にする。

doc.core> (run (<+> letter (many alpha-num)) "a177")
"a177"
nil

数字のパース

kern にはいくつか数値のパーサが用意されてて、これらは結果として文字列ではなく適切な数値型を返します。これらのパーサは名前空間 lexer 内のそれらに比べてサポートする数値型は少ないし、空白とかコメントとかもスキップしません。つまるところ、大きなパーサの低レベルの部分で使うべき関数みたいですね。

使用にあたって Kern の名前空間 core をロードします。

(use 'blancas.kern.core)

value はパーサを走らせてそのパース結果を返す関数です。引数としてパーサと文字列を受け取って、オプションとして文字列が何であるか(例えばファイルネームだとか)を示すことができます。

dec-num は十進数の正数をパースして結果として long 型を返します。

doc.core> (value dec-num "8086")
8086

oct-num は8進数の正数をパースして結果として long 型を返します。

doc.core> (value hex-num "747")
487

hex-num は16進数をパースして結果として long 型を返します。 0x が先頭につくものは許されません。

doc.core> (value hex-num "FEED")
65261

float-num は浮動小数点数をパースして結果として double 型を返します。指数表現はサポートされていません。 lexer 内の float-lit ではサポートされてます。

doc.core> (value float-num "3.1415927")
3.1415927

Kern のプリミティブパーサ

Kern にもプリミティブなパーサがあって、これらを組み合わせていくことで大きなパーサを作るというのが関数型らしいパーサの作り方なわけですが。 それらをちょっと見ていきます。 Kern けっこうかわいいわあ。

(use 'blancas.kern.core)

return は常に成功し、与えられた値を返す。

doc.core> (run (return "foo") "xyz")
"foo"
nil

fail は与えられたエラーメッセージとともに失敗する。

doc.core> (:ok (parse (fail "foo") "xyz"))
false
doc.core> (run (fail "this is bad") "xyz")
line 1 column 1
this is bad
nil

satisfy は与えられた述語を入力の最初の文字に適用し、それによって成功か失敗かが決まる。

doc.core> (run (satisfy #(= \x %)) "xyz")
\x
nil

any-char は入力された文字を返す。

doc.core> (run any-char "x")
\x
nil

letter は大文字小文字のどちらも成功する。

doc.core> (run letter "z")
\z
nil

lower, upper はそのまんま。

doc.core> (run lower "a")
\a
nil
doc.core> (run upper "M")
\M
nil

white-space は空白文字を受け付ける。

doc.core> (run white-space "\t")
\tab
nil

space はスペースだけ。 tab もまあそんなかんじ。

doc.core> (run space " ")
\space
nil

digit は \0 から \9 までの数字を受け付ける。 hex-digit はそれに加えて \A から \F まで。 oct-digit は \0 から \7 まで。

doc.core> (run digit "9")
\9
nil

alpha-num は英数字。

doc.core> (run (many alpha-num) "9A")
[\9 \A]
nil

sym* は特定の文字だけ。 sym- は大文字小文字関係なく。ただし結果として得る文字は sym- に渡された文字。

doc.core> (run (sym* \x) "x")
\x
nil

token* は Haskell でいう string パーサ。複数のシーケンスが渡された場合、どれかが成功するまで試され続ける。 token- は大文字小文字関係なく。

doc.core> (run (token* "foo" "bar" "baz") "bazaar")
"baz"
nil

word は token と同じようなものだけれど、許可しない終端を設定できる。 word- は大文字小文字関係なく。

doc.core> (run (word* (one-of* "|-/") "foobar"))
"foobar"
nil
doc.core> (run (word* (one-of* "|-/") "football" "foobar") "foobar*")
"foobar"
nil
doc.core> (run (word* (one-of* "|-/") "foobar") "foobar/")
line 1 column 8
unexpected /
expecting end of foobar
nil

one-of は与えられた文字列のどれかの文字であれば受け付ける。 none-of はその逆。

doc.core> (run (one-of* "xyz") "zap")
\z
nil

new-line* は \n を受け付ける。 eof はもし入力が空なら成功する。

field* は与えられた文字列で区切られる文字列をパースする。

doc.core> (run (field ",;") "California,")
"California"
nil

split-on は空白か与えられた文字列で区切られる文字列を分割する。

doc.core> (run (split-on ",./") "Now, is the time")
["Now" "is the time"]
nil

split は空白で区切られたテキストを分割する。

doc.core> (run split "Now is the time")
["Now" "is" "the" "time"]

Clojure の パーサコンビネータライブラリ Kern

Clojure でパースするとき、みんなどうしてるんだろう。 Parsec のようなものがあればいいのに〜、と思って探してみたところ、この Kern が一番よさげだった。

特徴

  • 状態モナドベースのコンビネータ
  • C, Java, Haskell, Shell の構文をサポート
  • パースと式の評価をサポート
  • 正確で詳しいエラーメッセージ
  • エラーメッセージは簡単に多言語化可能
  • パーサの内部状態にクライアントコードからアクセス可能
  • サンプルあり

セットアップ

leiningen がインストールされているなら、 project.clj の :dependencies に

[org.blancas/kern "0.6.1"]

で、コード中に

(use 'blancas.kern.core)

Haskell の Parsec っぽい

HaskellのParsecですねという感じ。というか記号とかもそのまんま。 ただちょいちょい違うところがあるかなあという印象。

例えば数字のパース。

doc.core> (run digit "123")
\1
nil
doc.core> (run (many digit) "123")
[\1 \2 \3]
nil

run はパース結果を印字する。パースしてない残りの入力はそのまま。パーサの状態は run* で見れる。

doc.core> (run* digit "123")
{:input (\2 \3),
 :pos {:src "", :line 1, :col 2},
 :value \1,
 :ok true,
 :empty false,
 :user nil,
 :error nil}
nil

文字列以外にも入力としてファイルとかも受け付ける。 runf は パーサとファイル名、文字コードを受け取ってパースする。

コンビネータ <*> は与えられたパーサを順に実行し結果をベクタにして返す。

doc.core> (run (<*> letter letter digit) "os8")
[\o \s \8]
nil

parse は run みたいな動作をするけれどパーサの状態を返すという点で異なる。 :ok フィールドをチェックすればパースに成功したかどうか分かる。 :value セレクタを用いればパーサの結果が得られる。

doc.core> (let [st (parse (token* "abc") "abc")]
        (when (:ok st) (str "got " (:value st))))
"got abc"