NemerleのMonadic Parser Combinator

最近、パーサの作成に関する素晴らしいMonadic Parser Combinatorの記事を発見しました。 主なアイデアは、パーサーを最初のクラスのオブジェクトとして記述することです。これにより、元の問題を分解し、より簡単なパーサーの合成の形で目的のパーサーを提示できます。 パーサーの組み合わせを容易にするために、成功アプローチのリストによる置換エラーが使用されます。 haskell言語では、パーサーの作成を扱うのが非常に便利です。このタスクは当然モナドの概念に当てはまるため、残念ながらモナドはnemerleではサポートされていませんが、言語はこのタスクを処理するのに十分強力です。



パーサーは、文字列からタプルのリストへのマッピングとしてより詳細に提示できます。タプルの最初の要素は文字列の解析された部分であり、2番目の要素は文字列の残りです。 解析に失敗した場合、空のリストが返されます。 場合によっては、文字列の解析部分と呼ばれるものは異なる型を持つことができるため、たとえば、 string- > list ['a * string ]のように、構文解析型をポリモーフィックと見なすのが自然です。 以下では、そのような構成をパーサー['a]として指定します。



次に、パーサーの組み合わせを検討する必要があります。 「sin42」を解析および計算する必要があるとします。数学関数を解析できるパーサー-math:parser [double-> double ]、および数字を解析できるパーサー-number:parser [ double ](through:type of parsers )

目的のパーサーは次のように記述できます。

def eval = text => math(text).Map((f、rest)=> number(rest).Map((a、b)=>(f(a)、b)))。Fold([]、 (e、acc)=> e + acc)


かなり容量が大きいことが判明しましたが、これが美しいコードであることに同意できるのは、FPの真っ赤なファンだけです。 以下では、パーサーで演算子を定義します。このコードは次の形式で書き直すことができます。

def eval = math *(f => number *(x => result (f(x))))


簡単に言えば、「*」演算子のアクションは、次のように動作する新しいパーサーを作成することとして説明できます。最初のパーサーを適用し、2番目のパーサーが最初の解析結果をパラメーター化し、解析を続行します。 入力演算子 '*'には、パーサー['a]と、' a->パーサー['b]などのパラメーター化を実行する関数があります。



以下に、このアプローチの単純さと力の例として、正の数、括弧、および「+」演算で構成される算術式の正確性をチェックするパーサーを作成しました。 コードはネメル語で書かれていますが、構文についてコメントし、セマンティクスを自分で理解しようとしますが、慎重に-頭を壊さないでください! また、コードのサイズについて心配する必要はありません。その半分はパーサーを作成するための別のライブラリに移動できるため、haskellの場合、このアプローチに基づくライブラリはparsecです。 行こう!

using System;

using System. Console ;




必要な名前空間を接続し、System.Consoleクラスを展開します。これ以降、その静的メソッドはすべてグローバル関数/プロシージャになります。 モジュール、デリゲートの代わりの機能タイプ、コンストラクターを呼び出す前のnewの欠如、thisという単語によるコンストラクターの説明については、おそらく以前に書いたでしょう。 ここで注意しなければならないのは、nemerleのジェネリックパラメーターは山かっこではなく角括弧で指定されていること、型名にアポストロフィを含めることができること、およびnemerlではアポストロフィで始まるジェネリックパラメーター名を付けることをお勧めすることです。

module Program

{

class Parser ['a]

{

public this (core : string -> list ['a* string ]) { this .core = core }

core : string -> list ['a* string ];






最初の要素がint型であり、nemerleの2番目の文字列がint * stringとして記述されているタプル型。タプル自体は、まだ記述していない場合は、たとえば次のように記述されます。def tuple =(45、 "qwerty") これで、リスト['a * string]エントリが明確になることを願っています。

public Parse(text : string ) : list ['a* string ] { core(text) }



public static @*['b](self : Parser ['a], parser : 'a-> Parser ['b]) : Parser ['b]

{

Parser ( fun (text : string ) : list['b* string ]

{

def data = self.Parse(text);

data.Map((ast,rest) => parser(ast).Parse(rest)).FoldRight([], (e,acc) => e+acc)

})

}







私の友人が考えたように、@ *は合致ではなく、C#の演算子*の同義語です。 このブログではまだ見たことのないもう1つの構文機能は楽しいです。 これはラムダ式にすぎず、ラムダのその他の可能な構文はC#の構文と一致します。たとえば、次のコード例は同等です。fun(x){x + 1} and x => x + 1; オプションで、たとえばfun(x:int):int {x + 1}またはx:int => x + 1などのタイプを指定できます。ラムダを書き込むことができる場合、1行で2番目のオプションを使用します。

public static @+(a : Parser ['a], b : Parser ['a]) : Parser ['a]

{

Parser (text => a.Parse(text) + b.Parse(text) )

}

}







リストの操作+はnonmerlで定義されており、上記のコードではラムダの本体内で使用されています。

class zero ['a] : Parser ['a] { public this () { base (x=>[]) } }



class result ['a] : Parser ['a]

{

public this (value : 'a) { base (text => [(value,text)]) }

}







Javaのように、Merle以外では、基本クラスコンストラクターはサブクラスコンストラクター内で呼び出されます。 私が定義した2つのクラスは基本的なパーサーであり、他の多くのパーサーとパーサージェネレーターはそれらに基づいています。 結果クラスは、haskellのモナドコンストラクターと比較できます。 パーサーを返す関数をパーサージェネレーターと呼びました。たとえば、シンボル(x:char)ジェネレーターは次のように定義されます。Parser [char]。解析行の先頭に指定されたシンボルを期待するパーサーを作成します。



以下に、3つのパーサーコンビネータを定義します。 このコードはすべて静的クラスであるモジュール内に記述されているため、このクラスの静的メソッドを簡単に宣言でき(この場合、これはモジュールなので静的キーワードは不要です)、グローバル関数として使用できます。 パーサーコンビネーターとは、1つまたは複数のパーサーを入力に取り込み、出力でパーサーを返す関数です。 取るに足らないコンビネータは恒等関数ですが、それはおもしろくないので、コンビネータmany、many1、oneOfを定義しました。 1つ目は入力のあるパーサーを繰り返しますが、解析する行はそれを許可します(正規表現のアナログ*)、2つ目は正規表現のアナログ+であり、3つ目はそのうちの1つまで順番に渡されたパーサーを適用しようとします文字列の一部を正常に解析しません。

many['a] (parser : Parser ['a]) : Parser [ list ['a]]

{

parser * (x => many(parser) * (xs => result (x::xs))) + result ([])

}



many1['a] (parser : Parser ['a]) : Parser [ list ['a]]

{

parser * (x => many(parser) * (xs => result (x::xs)))

}



oneOf['a] ( params parsers : array [ Parser ['a]]) : Parser ['a]

{

Parser ( fun (text){

parsers.FoldLeft([], fun (e,acc)

{

| (_, []) => e.Parse(text)

| (_, _) => acc

})

})

}







最後のジェネレーターでは、ラムダ式の本体はパターンマッチングに短縮構文を使用します。 関数の引数がパターンマッチングに関与し、関数の本体が1つのパターンマッチング式で構成されている場合に使用できます。 同等のコード例を示します。def foo(x){ match (x){| [] =>「空」| _ => "!empty"}}およびdef foo(x){| [] =>「空」| _ => "!empty"}。



注意!


以下は、チェックパーサーが作成する既に重要なコードです。 これが最も一般化されており、特定のパーサーに関連付けられていないため、これより前のすべてのコードをライブラリーに取り込むことができます。

Main() : void

{

def item = Parser(x => if (x.Length==0) [] else [(x[0],x.Substring(1))] );

def sat(predicate) { item * (x => if (predicate(x)) result (x) else zero ()) }

def symbol(x : char) { sat(y => x==y) }

def digit = sat(x => '0' <= x && x <= '9' );

def number = many1(digit);



def expr()

{

def parentheses =

symbol( '(' ) * (_ =>

expr() * ( _=>

symbol( ')' ) * (_ => result ([]))

)

);



def trivial = oneOf(number, parentheses);



def mul =

trivial * ( _ =>

many1(symbol( '*' ) * ( _ => trivial)) * (_ => result ([]))

);



def sarg = oneOf(mul, trivial);



def sum =

sarg * ( _ =>

many1(symbol( '+' ) * ( _ => sarg)) * (_ => result([]))

);



oneOf(sum,mul,trivial)

}



WriteLine(expr().Parse( "5+42*(8+1)" ));

_ = ReadLine();

}

}






このコードはすでに(構文的に)理解可能であるはずです。私が見落とした唯一のことは、defという単語の意味を説明していなかったことです。 この言葉は、宣言することの意味を定義する英語から来ています。 これがまさにそれです:値に名前を付けてローカル関数を宣言することができます(クラスメソッドや他のローカル関数に関連して)。 1つ目はC#のvarという単語の動作に似ていますが、メジャー以外では名前の値を変更することはできません。例を使用すると、これを理解しやすくなります。

def x = 1; //正しい

x = x + 1; //コンパイルエラー

def x = x + 1; //コンパイルは成功しました

可変 y = 1; // mutable-Cのvarの完全なアナログ#

y = y +1; //すべて順調です。



パーサーコードへのもう1つの注意:exprは、その説明がそれ自体を参照するため、ローカル関数として宣言されます。



PS記事のアイデアを適切に語り、読者にnemerle言語をより良く紹介できたことを願っています。



cc-by-nc-sa



All Articles