引き続き Mogul という名前のλ計算インタプリタを作っていこうと思います。
前回、λ式を表現するデータ型を定義したので、今回はパーザを書きます。ソースコードを読ませて抽象構文木を生成するところまでです。
ソースコード全体は GitHub に置いておきます。
Mogul の構文
パーザを書くにあたってまずは構文を決めなければいけません。
Mogul の構成要素で重要なのは 変数, 関数抽象, 関数適用 そして 関数定義 です。ぞれぞれ以下のような構文にしようと思います。
変数
変数の構成要素として許される文字は 英数字 + アンダースコア _
です。
ただし、英小文字は必ず1文字で一つの変数として解釈されます。英大文字, 数字, アンダースコアは複数連なっていればまとめて一つの変数だと解釈されます。
いくつかの例を挙げましょう、
a
や x
や FOO
や FOO_2
は全て正しい変数名と解釈されます。
xy
と書いた場合、これは二つの変数 x
と y
が連続したものだと解釈されます。同様に Foo
は三つの変数 F
, o
, o
が連続したものだと解釈されます。
驚くべきことに(?)、_
や 42
なども変数名として正しいと解釈されます。
常識的なプログラミング言語では 42
と書くと整数リテラルだと解釈されるでしょう。しかし幸か不幸かλ計算の世界に整数などというものは存在しません。なので 42
のように整数っぽく見えるものでも変数名として許してしまいます。
関数抽象
関数抽象は をほぼそのまま書けるようにします。唯一の違いは λ
の代わりに ^
を使うことです。
^x.M
と書くと、 のことだと解釈されます。
また、λ計算の世界には1変数関数しか存在しないので、複数の引数をもとに計算を行いたい場合には、
という感じで カリー化 を行います。
もちろん Mogul でも全ての関数は1変数関数として扱われますが、ネストした関数抽象のために以下のような糖衣構文を許すことにします。
^x y z.M
これは ^x.^y.^z.M
と書いたのと同じように解釈されます。
関数適用
前回の記事でも宣言したとおり、関数適用には Unlambda 記法を採用します。
すなわち x
に y
を適用するときは、バッククォート `
を使って
`xy
と書くことにします。
関数定義
関数定義の構文はこうです、
FLIP = ^f.^x.^y.``f y x
左辺に関数名、右辺に関数の本体を置き、左辺と右辺をイコール =
でつなぎます。
また、左辺に 仮引数 を伴った書き方も許すことにします。
```FLIP f x y = ``f y x
または間を取った以下のような書き方も正しいです。
`FLIP f = ^x.^y.``f y x
実はこれら三つの書き方には別の意味を持たせようと思っています。前回の記事にも出た アリティ(項数) の概念です。
ひとまず今は 仮引数を左辺に置くか右辺に置くかによって別の構文木を生成させる ということだけ頭の片隅に置いてもらえれば大丈夫です。
コメント
ついでに、Mogul ではラインコメントを書けるようにします。
コメントとして扱われるのは #
から行末までです。Python や Ruby と同じですね。
# 引数の適用順を入れ替える `FLIP f = ^x.^y. ``f y x # この部分はコメントです
ブロックコメント(複数行コメント)はありません。
BNF
というわけで、Mogul の構文を BNF で書き下すと、
# 識別子 <lower> ::= "a" | ... | "z" <upper> ::= "A" | ... | "Z" <digit> ::= "0" | ... | "9" <identifier> ::= <lower> | (<upper> | <digit> | "_")+ # λ式 <var> ::= <identifier> <lambda> ::= "^" (<identifier>)+ "." <expr> <apply> ::= "`" <expr> <expr> <expr> ::= <var> | <lambda> | <apply> # 関数定義 <func_name> ::= <identifier> <param> ::= <identifier> <ident_and_params> ::= <func_name> | "`" <ident_and_params> <param> <def> ::= <ident_and_params> "=" <expr> EOL # コンテキスト (関数定義の組) <context> ::= (<def>)*
このようになりますね。
これをもとにパーザを書いていきましょう。
使用するライブラリ
パーザコンビネータのライブラリとして、おなじみの parsec3 を使います。
Applicative スタイルで行を節約しながら書けるのがいい感じです。
今回は Text
型のストリームを読みながら抽象構文木に変換していくので、 Text.Parsec.Text
をインポートします。
パーザを書く
書いていきましょう。
識別子
-- | 識別子 ident :: Parser Ident ident = ident' <|> ident'' ident' :: Parser Ident ident' = Ident . singleton <$> lower ident'' :: Parser Ident ident'' = Ident . pack <$> many1 (upper <|> digit <|> char '_')
途中に登場している singleton
や pack
は、それぞれ Data.Text.singleton
と Data.Text.pack
です。
λ式
λ式は 変数, 関数抽象, 関数適用 の組み合わせで再帰的に定義されるので、それをそのままコードに落とし込みます。
-- | λ式 expr :: Parser Expr expr = apply <|> lambda <|> var -- | 変数 var :: Parser Expr var = Var <$> ident -- | 関数抽象 lambda :: Parser Expr lambda = do token $ char '^' v <- token ident vs <- many $ token ident token $ char '.' e <- token expr return $ mkLambda (v:vs) e where mkLambda vs e = foldr (:^) e vs -- | 関数適用 apply :: Parser Expr apply = do token $ char '`' e <- token expr e' <- token expr return $ e :$ e'
実はここでも Unlambda 記法を採用することのメリットが発揮されていて。Unlambda 記法だと var
と lambda
と apply
の動作が互いに排他になります。なので Text.Parsec.Prim.try
を使う必要がなくなって、コードがシンプルになり、頭の中で動作も追いやすくなってます。
関数定義
このパートでは関数名(Ident
)と無名関数(Func
)のタプルを取り出すことがゴールです。
-- | 関数定義の左辺部 "```f x y z" の形だけを許す defFunc :: Parser (Ident, [Ident]) defFunc = defFunc' <|> do funcName <- token ident return (funcName, []) defFunc' :: Parser (Ident, [Ident]) defFunc' = do token $ char '`' (funcName, args) <- token defFunc arg <- token ident return (funcName, arg:args) -- | 関数定義 def :: Parser (Ident, Func) def = do (funcName, reversedArgs) <- token defFunc token $ char '=' e <- token expr spaces' skipMany lineComment void endOfLine <|> eof return (funcName, Func (reverse reversedArgs) e)
def
の do 構文の中でなんだか汚いことやってますね。
関数定義は複数行に渡ってもよくて、かつ、どこかの行末で終わるはずで、さらに、ここでついでにラインコメントを読み飛ばそうとしているのでめっちゃ汚くなってます。
この部分を見通しよくするには、まずソースコードを lexer に通してコメントを読み飛ばしながらトークン列に変換して、次の段階でトークン列を parser に通して抽象構文木に変換するような実装にすればいいはずです。
が、この部分以外は lexer と parser に分けるまでもないくらいにシンプルなので作り込むのが面倒なんですよね。
ま、ちゃんと動いてますよ。一応テストも書いているし。
コンテキスト
コンテキストとは複数の関数定義の集まりです。
実装上では関数名(Ident
)と無名関数(Func
)の Map
になっています。
-- | コンテキスト (関数定義の組) context :: Parser Context context = Map.fromList <$> many1 def
def
が関数名と無名関数のタプルを返すようにしたので、many1
で複数個取ってきてから Data.Map.fromList
に食わせるだけですね。
ここで many
ではなく many1
を使った理由としては、...不明です。
ここなんで敢えて many1
を使っているんだろう。これだと空の文字列を読ませたときにパースエラーになりますね。このコード書いたのがざっくり2年前なので意図を忘れました。ここは後々書き換えるかも知れません。
使ってみる
実際にパーザにコードを読ませて抽象構文木を作ってみましょう。
ghci> parse expr "" "``skk" Right ((Var (Ident "s") :$ Var (Ident "k")) :$ Var (Ident "k")) ghci> parse expr "" "^xy.`yx" Right (Ident "x" :^ (Ident "y" :^ Var (Ident "y") :$ Var (Ident "x"))) ghci> parse expr "" "````s`k`sika^x.`xx" Right ((((Var (Ident "s") :$ (Var (Ident "k") :$ (Var (Ident "s") :$ Var (Ident "i")))) :$ Var (Ident "k")) :$ Var (Ident "a")) :$ (Ident "x" :^ Var (Ident "x") :$ Var (Ident "x")))
いい感じですね。
関数定義のほうも試してみます。
ghci> parse def "" "I = ^x.x" Right ( Ident "I" , Func { args = [] , bareExpr = Ident "x" :^ Var (Ident "x") })
読みやすいように整形してみました。
ちゃんと 関数名 Ident "I"
と 無名関数 Func {args = [], bareExpr = Ident "x" :^ Var (Ident "x")}
のタプルが返ってきてますね。
仮引数を左辺に置くか右辺に置くかによって別の構文木を生成させる と云ってた件、試してみます。
ghci> parse def "" "FLIP = ^f.^x.^y.``f y x" Right ( Ident "FLIP" , Func { args = [] , bareExpr = Ident "f" :^ (Ident "x" :^ (Ident "y" :^ (Var (Ident "f") :$ Var (Ident "y")) :$ Var (Ident "x"))) }) ghci> parse def "" "```FLIP f x y = ``f y x" Right ( Ident "FLIP" , Func { args = [Ident "f",Ident "x",Ident "y"], bareExpr = (Var (Ident "f") :$ Var (Ident "y")) :$ Var (Ident "x") }) ghci> parse def "" "`FLIP f = ^x.^y.``f y x" Right ( Ident "FLIP" , Func { args = [Ident "f"] , bareExpr = Ident "x" :^ (Ident "y" :^ (Var (Ident "f") :$ Var (Ident "y")) :$ Var (Ident "x")) })
関数定義の左辺に置いた仮引数は Func
型の args
フィールドの中で保持しています。
今のところの狙いは 左辺にいくつの仮引数が置かれていたかデータ型覚えておくこと です。
まとめ
Mogul の構文を整理しつつ、対応するパーザを書いてきました。
今回見せたパーザは何度も書き直しつつ洗練させてきたものなので少しは読みやすく書けてるんじゃないかと自負しております。
ソースコードを読んで抽象構文木を作れるようになったので、次回は プリティプリンタ を書いて抽象構文木をうまく印字できるようにします。
私からは以上です。