どーもです。イブのひと時如何お過ごしでしょうか。私事ながら、結婚とかしましてカップルお断り!な感じないいお店とか行けなくて残念って感じにイブを過ごすんじゃねーかな程度な師走時でございます。
という訳で最近始めましたなF#、といってもfsyacc/fslex を使ってパーサーを書いたりしかしてなかったり、しかもプロダクトコードというよりはビルド中に一定の解釈変換をしたい所の処理を書く為にやってるだけという酷い F# の使い方しかして無くて申し訳ない人ですが、その辺に関わるノウハウ的な物という事で書きたいと思います。
C# で定義された AST をfsyacc で出力する方法
やる事が DSL で出力回りに T4 とか使ってると C#コードからASTのトラバースとかしたいわけですが、C#側からF#で定義された判別共用体をグチャグチャいじるのは若干やりづらかったり、出力物はすでに .NET Framework 定義された現物を利用したくて AST を派手に定義したくなかったりする事ってありますよね。たとえば .NET Framework の Expression をパース結果として取りたい時とか。
という訳で F# Parsed Language Starter の AST を C# で置き換えてみましょう。
普通にプロジェクトをまずは作ります。そして C# クラスライブラリを作る事の Ast という事で、Ast 名前空間にぶつけます。


参照設定でF#での言語側プロジェクトから Ast クラスライブラリを参照します。

Class1.cs の内容を以下の様にして、fsy のアクションを以下の様に書き換えます。 Program.fs の eval 操作を削ると以下の通り。
namespace Ast
{
public class Factor {}
public class Term {}
public class Expr {}
public class Equation {}
internal class Integer : Factor {
internal int Value { get; set; }
}
internal class Float : Factor {
internal double Value { get; set; }
}
internal class ParenEx : Factor {
internal Expr Expr { get;set; }
}
internal class Times : Term {
internal Term Term { get; set; }
internal Factor Factor { get; set; }
}
internal class Divide : Term {
internal Term Term { get; set; }
internal Factor Factor { get; set; }
}
internal class TermFactor : Term {
internal Factor Factor { get; set; }
}
internal class Plus : Expr {
internal Expr Expr { get; set; }
internal Term Term { get; set; }
}
class Minus : Expr {
internal Expr Expr { get; set; }
internal Term Term { get; set; }
}
class ExprTerm : Expr {
internal Term Term { get; set; }
}
class ExprEquation : Equation {
internal Expr Expr { get; set; }
}
public class AstBuilder {
public static Factor Integer(int value)
{ return new Integer { Value=value }; }
public static Factor Float(double value)
{ return new Float { Value=value }; }
public static Factor ParenEx(Expr expr)
{ return new ParenEx { Expr=expr }; }
public static Term Times(Term term, Factor factor)
{ return new Times { Term=term, Factor=factor }; }
public static Term Divide(Term term, Factor factor)
{ return new Divide { Term=term, Factor=factor }; }
public static Term Factor(Factor factor)
{ return new TermFactor { Factor=factor }; }
public static Expr Plus(Expr expr, Term term)
{ return new Plus { Expr=expr, Term=term }; }
public static Expr Minus(Expr expr, Term term)
{ return new Minus { Expr=expr, Term=term }; }
public static Expr Term(Term term)
{ return new ExprTerm { Term=term }; }
public static Equation Equation(Expr expr)
{ return new ExprEquation { Expr=expr}; }
}
}
start: Prog { AstBuilder.Equation($1) }
Prog:
| Expr EOF { $1 }
Expr:
| Expr PLUS Term { AstBuilder.Plus($1, $3) }
| Expr MINUS Term { AstBuilder.Minus($1, $3) }
| Term { AstBuilder.Term($1) }
Term:
| Term ASTER Factor { AstBuilder.Times($1, $3) }
| Term SLASH Factor { AstBuilder.Divide($1, $3) }
| Factor { AstBuilder.Factor($1) }
Factor:
| FLOAT { AstBuilder.Float($1) }
| INT32 { AstBuilder.Integer($1) }
| LPAREN Expr RPAREN { AstBuilder.ParenEx($2) }
// This project type requires the F# PowerPack at http://fsharppowerpack.codeplex.com/releases
// Learn more about F# at http://fsharp.net
// Original project template by Jomo Fisher based on work of Brian McNamara, Don Syme and Matt Valerio
// This posting is provided “AS IS” with no warranties, and confers no rights.
open System
open Microsoft.FSharp.Text.Lexing
open Ast
open Lexer
open Parser
printfn “Calculator”
let rec readAndProcess() =
printf “:”
match Console.ReadLine() with
| “quit” -> ()
| expr ->
try
printfn “Lexing [%s]” expr
let lexbuff = LexBuffer<char>.FromString(expr)
printfn “Parsing…”
let equation = Parser.start Lexer.tokenize lexbuff
printfn “Evaluating Equation…”
let result = equation
printfn “Result: %s” (result.ToString())
with ex ->
printfn “Unhandled Exception: %s” ex.Message
readAndProcess()
readAndProcess()
めでたく
—— ビルド開始: プロジェクト: Ast, 構成: Debug Any CPU ——
Ast -> c:\users\kazuk\documents\visual studio 2010\Projects\FParsedLanguageStarter1\Ast\bin\Debug\Ast.dll
—— ビルド開始: プロジェクト: FParsedLanguageStarter1, 構成: Debug x86 ——
FParsedLanguageStarter1 -> c:\users\kazuk\documents\visual studio 2010\Projects\FParsedLanguageStarter1\FParsedLanguageStarter1\bin\Debug\Language.exe
========== ビルド: 正常終了または最新の状態 2、失敗 0、スキップ 0 ==========
ビルド成功と。
internal 宣言をマメにやってますが、Ast で internal になっている要素はパーサー実装にとっては要らない子という事です(実際に見えませんがビルドは通ってるのは上記のとおり)
ましてや判別共用体である事やムニャムニャはちっともパーサーにとっては必要のない事なわけです。これで式木をASTとして出力するパーサーを書けと言われても簡単なお話でExpression のノードを保持する型を EBNF の出力部に合せた型で保持して返すだけでよいという事も解るわけですね。
C# からのパーサーの呼び出し
ASTをF#以外の言語でもできたわけで、これでF#固有の言語セマンティックスである判別共用体をC#からモニャモニャする方法を悩まなくて良い様になりました。これでC#コードであるT4からASTを扱う処理は普通に書けばいいでしょって事になります。
ではパーサーを呼ぶ方法です。これが出来ないとT4から呼ぶとか、カスタムツールから呼ぶとか、ビルド時タスクから呼ぶとかパーサーが本当に欲しい人にとっては意味がないよって事で、ここをどうするかのお話になります。
まず単純にやってみて打ちひしがれましょう。
F#のパーサーアセンブリの出力をコンソールアプリケーションからクラスライブラリに変更します。

いきなりビルドできなくなります。(一撃目)

とりあえず Program.fs を全部コメントアウトしてビルドを通そうとしても無駄で、同じエラーになり、ちゃんとF#を勉強してこいと蹴っ飛ばされます。(二撃目)
仕方ないので Program.fs を削除!するのはもったいないのでビルドアクションにNoneを指定してコンパイル範囲から外してビルドを通します。

また C# のクラスライブラリを作ってF#のパーサープロジェクトを参照させます。


どんな型が定義されてるのかなと、オブジェクトブラウザを開こうとして打ちひしがれます。

以上、ありがとうございました。と終わると暗にF#をdisるエントリーの出来上がりです。
F# Advent Calender でそれをやる勇気はないので、ちゃんと呼べるようにしましょう。
namespace ParserExpose
open Microsoft.FSharp.Text.Lexing
open Ast
open Lexer
open Parser
type PaserExpose =
static member Parse (text:string) =
let lexbuff = LexBuffer<char>.FromString(text)
Parser.start Lexer.tokenize lexbuff
単純に LexBuffer<char> を構築して Parser.start を呼ぶだけです。
これを呼び出すメソッドは以下の通り。
using ParserExpose;
namespace ParserConsumer
{
public class Class1 {
public void DoParse(string text)
{
var equation = PaserExpose.Parse(text);
}
}
}
簡単に静的メソッドを呼ぶだけですね。
ここまでくれば T4 でなんかするのも簡単です。
見えなくても良いよを示す為に internal にしたのが裏目に出たのでサックリと InternalsVisibleTo で Ast アセンブリのinternal をParserConsumer に公開。
前処理されたテキストテンプレートを追加

こんな T4 Template を書くと
<#@ template language="C#" #>
<#@ import namespace="Ast" #>
<#+
public void WriteEquation( Equation equation )
{
var equ = equation as ExprEquation;
#><span class="equation"><#+ WriteExpr( equ.Expr ); #></span><#+
}
public void WriteExpr( Expr expr )
{
var p = expr as Plus;
if( p!=null ) {
#><span class="expr plus"><#+ WriteExpr( p.Expr );#>+<#+ WriteTerm( p.Term ); #></span><#+
}
var m= expr as Minus;
if( m!=null ) {
#><span class="expr minus"><#+ WriteExpr( m.Expr );#>-<#+ WriteTerm( m.Term ); #></span><#+
}
var t = expr as ExprTerm;
if( t!=null ) {
#><span class="expr"><#+ WriteTerm( t.Term );#></span><#+
}
}
public void WriteTerm( Term term )
{
var t = term as Times;
if( t!=null ) {
#><span class="term times"><#+ WriteTerm( t.Term );#>*<#+ WriteFactor( t.Factor ); #></span><#+
}
var d = term as Divide;
if( d!=null ) {
#><span class="term divide"><#+ WriteTerm( d.Term );#>/<#+ WriteFactor( d.Factor ); #></span><#+
}
var f = term as TermFactor;
if( f!=null ) {
#><span class="term"><#+ WriteFactor( f.Factor ); #></span><#+
}
}
public void WriteFactor( Factor factor )
{
var i = factor as Integer;
if( i!=null ) {
#><span class="factor integer"><#= i.Value #></span><#+
}
var f = factor as Float;
if( f!=null ) {
#><span class="factor float"><#= f.Value #></span><#+
}
var p = factor as ParenEx;
if( p!=null ) {
#><span class="factor">(<#+ WriteExpr( p.Expr ); #>)</span><#+
}
}
#>
これでspanでマークアップされたHTMLフラグメントになるわけですね。
マトメ
fsyacc/fslexは作るASTは外部定義の物を簡単に使えるし、作ったパーサーもC#から呼ぶための口を開いてあげれば普通に呼べるしという事で、使えば色々使える代物でございます。
バイナリーのフォーマット定義とか C言語での構造体記述が共通記述言語だったりしますよね、それを手で変換するとかしてたなら軽くパーサー書いてあげればT4でコードに落としたりできるし、バイナリーフォーマットのDSLたるC言語の構造体表現に対応した事になるわけです。その表現に従って BinaryWriter / BinaryReader を連打するコードを吐かせれば古き良きバイナリープロトコルでのOLTPもどんと来いです。
LALR(1)のパーサーが出来るのですから頑張れば C# のコードもパースできるはずです。あなたのマシンにも入ってるC#言語仕様のドキュメントのEBNFを引っ張りだして C#コードに埋められたDSLを解釈しても良いでしょう。
パーサーが必要になるのはDSLに限りません、Lexer / LexBuff は char のシーケンス処理です。ゲームパッドの上下左右やボタンを文字にマップして流し込めば UUDDLRLRBA でコナミコマンドを解釈するなんてのもきっと出来るでしょう。連続するポーズを文字にマップして解釈すればKinectでポーズを読んで仮面ライダーの変身ポーズを Accept するパーサーだって書けるはずですね。
簡単に流れ制御構文をいくつか受け取ってWindows Workflow の流れ制御系アクティビティのインスタンスを生成する事ができるパーサーとか書くと、「俺たちはテキストなコードを書くドメインスペシャリストなんだが、MSの中の人達なんか勘違いしてないか?」とか言えますね。見た目かっこよく素人受けの良いフォーマットでとかは生成されたアクティビティを XamlWriter に渡してXAML書かせてVSでデザイナ表示すればできる事
マトメのマトメ
メソッドをちゃんと定義してあげても結局の所、オブジェクトブラウザで見ようとすると、こうなるのは。
—————————
Microsoft Visual Studio
—————————
利用可能でないか、またはビルドされていないため、このプロジェクトをオブジェクト ブラウザーで表示することができません。プロジェクトが利用可能でビルド済みであることを確認してください。
—————————
OK
—————————
F#たんのこの画像の雰囲気どおりの仕様で良いと思います。図書館の文学少女は誰がなんと言おうと正義です。出来上がったアセンブリがVSで表示できないからとILDASMに読み込むときの背徳感最高です。文学少女を体育倉庫に連れ込んでイケナイ事をしてる妄想がテンションを上げてくれます。F#たんサイコーです。