About Expandable F# Compiler project – F# Advent Calendar 2016 (Japan)

christmas_zangyou_manこの記事は、「F# Advent Calendar 2016」の23日目の記事です。やっばいギリギリ。

英語版はこちら (For English post: https://www.kekyo.net/2016/12/23/6305)

前日は、ぜくるさんのTypeProviderのお話です。

今私は、「F#のコンパイラの代替え品」を作っています。とは言っても、一からF#のコンパイラを自作できるような技量はあるはずがなく、「F# Compiler Service」をバックエンドとして使っています。

F# Compiler Serviceは、C#でいう所のRoslynに相当して、F#ソースコードをパースして構文木(Abstract Syntax Tree)を取得し、さらにそれを使って実際にコンパイル(つまりMSILを生成してPEを出力する)ところまでをプログラマブルに実行できるライブラリです(他にも、fsiに相当するInteractive interpreterのエンジンも持っています)。

何故、代替えとなるコンパイラを作ろうとしているのかとか、背景についての紹介をしたいと思います。


Expandable F# Compiler project (fscx)

fscx_512Expandable F# Compiler (fscx)は、GitHubで公開しています。ちなみに現在のバージョンは0.6.14、まだ正式リリースには至っていません。仕様は予告なく変更される可能性があります :)

fscxは、標準のF#コンパイラ(fsc.exe)の代わりに使用します。目的とかポイントは以下の通りです:

  • コンパイル時に「ソースコードの自動変形」を可能にしたい – これは、ソースコードのテキストベースの置換(例えば正規表現などを使って)するのではなく、F# Compiler Serviceで構文木を取得した後、その構文木に直接手を入れて変形させることを目指しています。テキストベースの置換では、明らかに不正な変形を行ってしまう危険性がありますが、構文木ベースであればそのような問題は発生しません(実際には非常に難しいのですが)。
  • この自動変形を行う処理を「フィルタライブラリ」と呼び、フィルタライブラリは自由に拡張出来るようにする – 例えば、当初想定しているのは、目的となるメソッドの呼び出しコードの前後に、自動的に追加コードを(構文木ベースで)挿入するフィルタライブラリです。しかし、これに限らず、様々な変形をプラガブルに実現できるようにします。
  • 導入が簡単である事 – コンパイラパスの手動構成やVSIXではなく、「NuGetパッケージ」として公開し、NuGetインストールするだけでfscxを使用するようにできる事。これは、以前「NuGetでビルドプロセスに介入するパッケージを作る」で書いた通り、MSBuildのスクリプトを書くことで実現できました。

使用者から見た構造

slide1fscxの使用者は、fscx自体を直接扱うわけではありません。目的別に作られた「フィルタライブラリ」がNuGetで導入できるようになるため、これを導入することで、依存関係によりfscxが導入されて、標準のfsc.exeを置き換えます。

この図では、「sample-filter.nupkg」というNuGetのパッケージがあり、これを自分のF#プロジェクトに導入すると、fscx(実際には”FSharp.Expandable.Compiler.Build”)も一緒に導入され、fsc.exeではなく、fscxを使ってコンパイルを行うようになります。そして、sample-filter.nupkgに含まれるフィルタライブラリを使って構文木を変形し、バイナリを生成します。

例えば、以下のようなコードを書いていたとします:

module SampleModule =
  let format (a: int) (b: int) (c: string) =
    System.String.Format("{0} = {1}", a + b, c)

もし、sample-filterが「関数(メソッド)の呼び出し時にログ出力を挿入する」フィルタライブラリだったとすると:

module SampleModule =
  let format (a: int) (b: int) (c: string) =
    // 以下のコードが自動的に挿入される
    System.Diagnostics.Trace.WriteLine("Log: format {0} {1} {2}", a, b, c)
    System.String.Format("{0} = {1}", a + b, c)

のような変形を「コンパイル時」に実現します。もちろん、ソースコードファイル自体に一切変更は加えません。そして、使用者はsample-filter.nupkgを導入するだけでこれが実現できるようになります。

使用者がフィルタの詳細に関与することなく、フィルタパッケージの導入だけで実現する – このようなツールセットが透過的に振る舞う必要があると言う点で、非常に重要だと考えています。

フィルタライブラリ開発者から見た構造

slide2フィルタライブラリ側は、やや複雑です。

まず、フィルタ処理はF# Compiler Serviceの構文木を使うため、必ずF# Compiler Serviceへの参照が必要になります。また、fscxの中核となるライブラリも参照が必要です。fscxは”FSharp.Expandable.Compiler.Core”で公開されているので、自分のフィルタライブラリプロジェクトに導入しておきます。

そして、実際にフィルタコードを書けばよいのです…が…


フィルタコードの実装手段

フィルタライブラリを作る際に、2通りの方法を選択することが出来ます:

  • 構文木に対して、レガシーなビジターパターンを使う – これは、.NET LINQで使われている「ExpressionVisitorクラス」と同じようなクラスを使って、いわゆるOOPベースのビジターパターンで構文木の変形を行う方法です。なぜExpressionVisitorクラスをそのまま使わないのか?と思うかもしれません。これはF# Compiler ServiceやRoslynを使ったことがある方ならわかると思いますが、構文木の構造は言語毎に大きく異なるのです。同じRoslynでも、実はC#とVisual Basicが似て異なる構文木クラス群を使うように、F#も全く異なる構文木型を使います。つまり、既存のVisitorクラスはどれも流用出来ないため、専用の基底Visitorクラスを用意しています。
  • F# Compiler Serviceは、ビジターパターンを適用できる標準的なインフラを用意していません – 何故なら、関数プログラミングとパターンマッチングを使うと、わざわざ基底Visitorクラスのようなものを用意しなくても、簡単にビジターパターンを適用できるからです。とは言っても、再帰的な探索のうち、構文木のリストなどは式で書くと煩雑になりがちです。そこで、関数プログラミングスタイルで使用できる、再帰探索の実装を簡略化可能な補助ライブラリを用意しました。

例えば、基底Visitorクラスを使うと、以下のようにコードを書くことが出来ます:

type InsertLoggingVisitor() =
  inherit AstInheritableVisitor()

  override __.VisitExpr_Quote(context, operator, isRaw, quoteSynExpr, isFromQueryExpression, range) =
    // DEBUG
    printfn "%A" operator
    base.VisitExpr_Quote(context, operator, isRaw, quoteSynExpr, isFromQueryExpression, range)

  override this.VisitExpr_App(context, exprAtomicFlag, isInfix, funcExpr, argExpr, range) =
    match funcExpr with
      // ...

関数ビジターパターンのための補助ライブラリを使った場合:

let outerVisitor
   (defaultVisitor: (NoContext * SynExpr -> SynExpr),
    context: NoContext,  // (Non custom context type)
    expr: SynExpr) : SynExpr option =

  match expr with
  | SynExpr.Quote(operator, _, _, _, _) ->
    // DEBUG
    printfn "%A" operator
    None  // (None is default visiting)

  | SynExpr.App(exprAtomicFlag, isInfix, funcExpr, argExpr, range) ->
    match funcExpr with
      // ...

  | _ ->
    None  // (None is default visiting)

どちらが望ましいかは、一長一短あります。基底Visitorクラスを使う場合は、すべての構文木ノードをビジターの対象に出来ますが、ネストした構文木の解析は煩雑です。関数ビジターパターンは解析の自由度が高く、パターンマッチングを使用して容易に構文木を特定できますが、ビジターの対象はSynExprのみです。

これらの補助ライブラリは、fscx自身に直接依存しないため、「FSharp.Compiler.Service.Visitors」という独立したパッケージにしてあります。そのため、fscxを使わないとしても、このライブラリだけで、F# Compiler Serviceのビジターパターンの実装に流用することもできます。

# このライブラリは0.7.1で大体固まった感があるので、正式リリースまで大きく変わらないと思います。


構文木変形の難しさ

構文木の変形には、一種のロマンがあります(たぶん ;)

しかし、実際には「死ぬほど大変」です。この記事を読んでいる人は、おそらくF# Compiler ServiceやRoslynを日常的に触っていると思う :) ので、それがいかに大変かおわかりいただけると思います。LINQのExpressionTreeも大概ですが、あれが更に複雑になったものと思ってもらえれば良いと思います。

# なお、ExpressionTreeについては、Advent LINQの後半の記事や、Final LINQ Extensions IIIが参考になると思います。

前述のsample-filterで示したような変形を実現するには、狙った場所に対応する構文木がどのような構造で現れ、それをどのように変形すれば目的を達成できるのかを、慎重に検討しなければなりません。

構文木は再帰構造で定義されます。そのため、いい加減な判定を行うと、意図しない箇所を変形してしまう可能性があります。また、ソースコード上では些細な違いでも、構文木上ではドラスティックに変わってしまったりすることもあります。

例えば、以下のようなコードを考えます:

// 一般的な.NETのメソッド(タプル形式)
let output1 (a: int, b: string, c: double) =
  System.Console.WriteLine("output1: {0}:{1}:{2}", a, b, c)

// F#関数(カリー化可能形式)
let output2 (a: int) (b: string) (c: double) =
  System.Console.WriteLine("output2: {0}:{1}:{2}", a, b, c)

output1は、典型的な.NET標準のメソッドと呼べるでしょう。しかし、output2はF#らしい「関数」です。このoutput2を呼び出すコードを書いたとします:

// 一般的な.NETのメソッド呼び出し(タプル形式)
// (int * string * double) -> unit
output1 (123, "ABC", 456.789)

// F#関数の呼び出し(カリー化可能形式)
// int -> string -> double -> unit
output2 123 "ABC" 456.789

.NETメソッド形式は、以下のF#関数と区別して「タプル形式」と呼んでいます。構文木上でも、引数部分が「タプル」のように見え、一つのタプル値を引数に渡しているかのように解釈されます。F#関数は「カリー化可能形式」です。カリー化可能の場合の関数呼び出しは、部分適用された関数が連続的に適用されていくような感じに解釈されます。そして、構文木上もネストされた関数呼び出し(Appノード)で表現されます。

構文木が可視化できないと、ここら辺の検討が絶望的に大変なので、小さなツールですが構文木をF#コードっぽくダンプするツールを作りました(可視化が目的だったので、完成度は高くありません)。これを使うと、以下のような出力が得られます:

// 以下がoutput1の呼び出し
Ast.SynExpr.App( (* expr1 *)          // App - output1(...)
  ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
  false (* isInfix *),
  Ast.SynExpr.Ident( (* funcExpr *)
    output1 (* Item *)),
  Ast.SynExpr.Paren( (* argExpr *)    // Paren --> Tuple : タプル形式の引数
    Ast.SynExpr.Tuple( (* expr *)
      [ (* exprs *)                   // タプルの中身をリストで表現
        Ast.SynExpr.Const( (* [0] *)
          Ast.SynConst.Int32( (* constant *)
            123 (* Item *)));
        Ast.SynExpr.Const( (* [1] *)
          Ast.SynConst.String( (* constant *)
            "ABC" (* text *)));
        Ast.SynExpr.Const( (* [2] *)
          Ast.SynConst.Double( (* constant *)
            456.789 (* Item *)))],
      [ (* commaRanges *)]),
    range>.Some( (* rightParenRange *)))),

// 以下がoutput2の呼び出し
Ast.SynExpr.App( (* expr2 *)          // App - 456.789の適用
  ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
  false (* isInfix *),
  Ast.SynExpr.App( (* funcExpr *)     // App - "ABC"の適用
    ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
    false (* isInfix *),
    Ast.SynExpr.App( (* funcExpr *)   // App - output2に対して123の適用
      ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
      false (* isInfix *),
      Ast.SynExpr.Ident( (* funcExpr *)
        output2 (* Item *)),
      Ast.SynExpr.Const( (* argExpr *)
        Ast.SynConst.Int32( (* constant *)
          123 (* Item *)))),
    Ast.SynExpr.Const( (* argExpr *)
      Ast.SynConst.String( (* constant *)
        "ABC" (* text *)))),
  Ast.SynExpr.Const( (* argExpr *)
    Ast.SynConst.Double( (* constant *)
      456.789 (* Item *))))),

出力が冗長で見難いのですが、output1とoutput2では、引数がリストで表現されているか、ネストされた関数として表現されているかで全く構造が異なることが分かります。カリー化可能形式とは、実は以下のような呼び出しであることが分かります:

// F#関数の呼び出し(カリー化可能形式)
// int -> string -> double -> unit
output2 123 "ABC" 456.789

// 実はこういう事:
((output2 123) "ABC") 456.789

// さらに分解すると:
let f1 = output2 123  // f1は高階関数
let f2 = f1 "ABC"     // f2は高階関数
f2 456.789

ソースコードの些細な違いが、構文木上で大幅に異なる表現となる場合があるため、ビジターパターンで探索する場合に注意が必要です。実際のところ非常に難しい…

# 余談ですが、構文木の解析が非常に大変だった経験から、先日のNGK2016BのLTネタ: 「You will be assimilated. Resistance is futile.」が生み出された、みたいな背景があります。Roslynなので、fscxとは全然関係ないのですが :)


フィルタのミドルウェア

このように、フィルタライブラリの独自実装自体は、構文木の扱いが非常に複雑であるため、特に想定されるシナリオのために、ミドルウェアとなる補助ライブラリ”FSharp.Expandable.Compiler.Aspect”を用意しています。

これは、「AOP(アスペクト志向パラダイム: Aspect-Oriented-Paradigm)」で知られているような、任意の処理の直前と直後に、安全に処理を挿入できる機構を持った手法と同じことを、fscx上で実現させるための補助ライブラリです。

フィルタライブラリを開発する際に、一からフィルタライブラリを実装するのではなく、この補助ライブラリを使うと、fsprojのプロパティに指定した情報と「アスペクトクラス」を指定して、構文木の操作を一切実装することなく、安全にAOPを実現できます。

例えば、以下のようなコードを用意しておきます:

// コンテキストクラス(関数を抜ける際に実行するメソッドを定義)
type SampleAspectContext internal (body: string) =
  // Finish aspect (trigger are leaved method with return value)
  member __.Leave(result: 'T) : 'T =
    Console.WriteLine("Leave: " + body)
    result
  // Finish aspect (trigger are leaved method with exception)
  member __.Caught(ex: exn) : unit =
    Console.WriteLine("Caught: " + body + ": " + ex.ToString())

// 関数を呼び出す際に呼び出されるメソッドを定義
type SampleAspect() =
  // Start aspect (trigger are entered method)
  static member Enter(methodName: string, fileName: string, line: int, column: int, args: obj[]) =
    let body =
      String.Format
        ("{0}({1}, {2}): {3}({4})",
         fileName,
         line,
         column,
         methodName,
         String.Join(", ", args |> Seq.map (sprintf "%A")))
    Console.WriteLine("Enter: " + body)
    new SampleAspectContext(body)

このアスペクトコードは、関数の入り口と出口で、自動的にコンソールにログを出力します。SampleAspect.Enterが関数の入り口で呼び出され、関数名やソースコードの位置、そして渡された引数を入手できます。この情報をもとに、ログを出力しています。

また、Enterが返すクラスのインスタンスが「アスペクトコンテキスト」として扱われ、LeaveまたはCaughtが関数の出口で呼び出されます。命名の通り、正常に抜ける場合にはLeaveが、例外で抜ける場合にはCaughtが呼び出されます。それぞれ、戻り値と例外のインスタンスが引数で指定されるので、これを元にログを出力しています。

これらのクラスや関数とその引数は、完全なダックタイピングです。特定のクラスやインターフェイスの実装は不要で認識されます。本当はインターフェイスで縛ったりしたいのですが、そうすると余計なアセンブリ参照が増え、最終的にはNuGetのパッケージングで非常に苦労することになるからです。

これで、アスペクトを「SampleAspect」として定義できたので、FSharp.Expandable.Compiler.Aspectに認識させるために、以下のコードを定義しておきます:

// SampleAspectクラスをアスペクトとして使用するフィルタを定義する
[<Sealed>]
type SampleAspectLoggerDeclaration() =
  // 依存関係を増やさないために、クラス名を文字列で指定
  inherit DeclareFscxInjectAspectVisitor("SampleAspectLogger.SampleAspect")

アスペクトコード自体とこの定義型の実装は、C#でも記述できるように、型の要求を緩くしてあります。

この定義がフィルタライブラリに含まれていると、fscxによって以下の操作が行われます:

  • 対象の関数の入り口に、SampleAspect.Enterを呼び出すコードを、構文木の挿入で実現する。
  • 対象の関数の出口に、SampleAspectContext.LeaveまたはCaughtを呼び出すコードを、構文木の挿入で実現する。Leaveは正常終了した際の戻り値を、Caughtはtry-withブロックによってキャッチした例外を伴って呼び出す。

このようなコードが:

let f11 (a: int, b: string, c: int) =
  output1(a + c, b, 123.456)

fscxによってこのように変形されます:

let f11 (a: int, b: string, c: int) =
  let __arg_0 = a + c
  let __arg_1 = b
  let __arg_2 = 123.456
  let __context =
    SampleAspectLogger.SampleAspect.Enter
      ("f11", "SampleCode.fs", 2, 3, [|__arg_0, __arg_1, __arg_2|])
  try
    __context.Leave(output1(__arg_0, __arg_1, __arg_2))
  with
  | ex ->
    __context.Caught(ex)
    reraise()

ここでは、タプル形式のメソッドを呼び出していますが、当然カリー化可能形式にも対応しています。

アスペクトクラス(SampleAspect, SampleAspectContext)と、アスペクトクラスを定義する型(SampleAspectLoggerDeclaration)以外に、構文木を操作するためのコードは一切不要であることに注目してください。

なお、現在FSharp.Expandable.Compiler.Aspectは実装を行っている最中で、特にどの関数を変形のターゲットとするかという指定方法を詰めている所です。


まとめ

このように、fscxはこれを土台として、FSharp.Expandable.Compiler.Aspectを使えばAOPを簡単に実現させることも出来、それ以上の変形も(難易度は高いですが)自在に可能でかつ、これをフィルタライブラリとして配布・再利用可能にします。

例えば、F#でOOPをやるのは非常にダルい説があります。何故なら関数プログラミングスタイルになれると、クラスとかインターフェイスを定義したり使ったりするのが面倒なためです(人によるかも知れませんが)。

そのようなわけで、F#でUIプログラミングをやるために、INotifyPropertyChangedのハンドリングとかも面倒この上ないのですが、fscxがあれば、インターフェイスの自動実装を行う変形フィルタライブラリを作ったりとかも出来るのです(しかも、コンパイル時に変形するのでランタイムコストがありません)。そういうライブラリがあれば、普通にコードを書いて、フィルタライブラリのパッケージをNuGetで導入するだけで、即自動実装させることが出来ます。

正式リリースまでには、まだいくつか詰める必要があるのですが、着地点は見えてきた感じです。

謝辞: 株式会社オンザロードさんと、ぶれいすさんに、このプロジェクトの協力を頂いています。ありがとうございます。

今年も残すところあとわずかですね。それではまた!

About Expandable F# Compiler project – F# Advent Calendar in English 2016

This post is 23th “F# Advent Calendar in English 2016.”

For Japanese post: (https://www.kekyo.net/2016/12/23/6263)

Hi, all F#er!! My name is Kouji Matsui, I live in Japan and I’m architect/developer.

I’m interesting to C#, LINQ, F#, IL and metaprogramming. I know about what/how asynchronosly execution between user mode .NET handler and kernel-mode driver DPC/hardware interruption. Titled Microsoft MVP for VS and DevTech. Holds CSM, CSPO. And bicycle hobby rider. I’m already released “F# FusionTasks.”

If you want contact to me, please use twitter: @kekyo2 or gitter: kekyo/public

# Sorry I have poor English, this post is very challenging for me :)

Currently, I’m developing an alternate compiler for F#. This is not meaning I’m compiler specialist :) This project uses the “F# Compiler Service.”

F# Compiler Service is likely “Roslyn” under C#. That’s parsing for F# source code and output AST (Abstract syntax tree). Then can compile and output to executable binary from ASTs. Another usage, F# Compiler Service have capability of interactive interpreter for F# (backend for fsi).

I try to write this project’s background: Why do I develop alternate compiler for F#?


Expandable F# Compiler project (fscx)

fscx_512“Expandable F# Compiler (fscx)” is an open source project on GitHub. Current version is 0.6.14, not reached official release. This post’s details will be implicitly changing :)

We can replace standard F# compiler (fsc.exe) to fscx. fscx’s goals are:

  • I want to auto translate the F# source codes – We can translate source codes by text-based (For example use by regular expressions). But this project not uses text-based translate. I’m designing for use F# Compiler Service and get ASTs, then translate ASTs directly. Because try with text-based translates then broken codes easier. AST based approach will not break and safe (But it’s very difficult for me, see below section :)
  • This auto translate library called “Filter library” and I want to can be extendable and expandable for non limitation – For example, a filter library can handles for insert another code fragments to before/after method body (by uses AST translate. likely AOP) Sure this is example, we can handle ANYTHING by AST translations.
  • Better easier installation – NOT customize for compiler executable path, NOT require install VSIX plugin. fscx is packaging by NuGet, we can use fscx on target F# project, require only fscx install via NuGet, simply then enables fscx. This senario already examine how to effect for building system by MSBuild (Post: “How to intercept for building system by NuGet,” In Japanese)

Structure viewed from the filter library user

slide1For fscx users, fscx uses not directly. We are implicitly using for fscx depended from the “Filter library” via NuGet. We can select filter library from the purpose. If installed fscx then auto replace the F# compiler (fsc.exe) to fscx.

For example, this slide identicate the box “sample-filter.nupkg.” This is packaged filter library on NuGet. We download and install this package via NuGet. NuGet client auto install depended packages include fscx named “FSharp.Expandable.Compiler.Build.” And ready for auto translate ASTs by filter library and fscx. Then compile and output translated binary.

Example in this code fragment:

module SampleModule =
  let format (a: int) (b: int) (c: string) =
    System.String.Format("{0} = {1}", a + b, c)

If “sample-filter.nupkg” contains AST translator implementation for: “Insert logging codes on function entry point:”

module SampleModule =
  let format (a: int) (b: int) (c: string) =
    // Auto inserted logging code below:
    System.Diagnostics.Trace.WriteLine("Log: format {0} {1} {2}", a, b, c)
    System.String.Format("{0} = {1}", a + b, c)

This translation executes at compile time. Sure no text-based translation. Then we can effect this translation for only installing filter library via NuGet.

Very important at this point: Users don’t know about filter implementation details. I’m designing for fscx to do in transparents.

Structure viewed from the filter library developer

slide2More complex for filter library side.

Filter library must use F# Compiler Service’s AST types, so library has assembly reference to F# Compiler Service and the fscx core library too. fscx core library package is naming for “FSharp.Expandable.Compiler.Core.” This package install to filter library project via NuGet.

Ready for can develop filter implemens, but…


How to implemens filter library

You can choice “Visitor patterns” on the helper library:

  • The legacy OOP based visitor pattern – This is most known technics for recursive tree structure on OOP. This approach likely .NET LINQ’s “ExpressionVisitor class,” named “AstInheritableVisitor class.” Why do we use AstInheritableVisitor instead ExpressionVisitor? Because F#, C# or any other languages (include .NET Expression-tree) different expression nodes smaller and larger. (You are already known things if you are using for F# Compiler Service or/and Roslyn :)
  • F# Compiler Service not provides standard visitor pattern infrastructures – Because we can implement visitor pattern with both the functional programming (FP) technics and pattern matching. But code fragments must has frequently recursive calls. So I developed visitor pattern helper functions on FP-based contains recursive callers.

For example, How to do using for OOP based visitor class:

type InsertLoggingVisitor() =
  inherit AstInheritableVisitor()

  override __.VisitExpr_Quote(context, operator, isRaw, quoteSynExpr, isFromQueryExpression, range) =
    // DEBUG
    printfn "%A" operator
    base.VisitExpr_Quote(context, operator, isRaw, quoteSynExpr, isFromQueryExpression, range)

  override this.VisitExpr_App(context, exprAtomicFlag, isInfix, funcExpr, argExpr, range) =
    match funcExpr with
      // ...

How to do using for FP based helper functions:

let outerVisitor
   (defaultVisitor: (NoContext * SynExpr -> SynExpr),
    context: NoContext,  // (Non custom context type)
    expr: SynExpr) : SynExpr option =

  match expr with
  | SynExpr.Quote(operator, _, _, _, _) ->
    // DEBUG
    printfn "%A" operator
    None  // (None is default visiting)

  | SynExpr.App(exprAtomicFlag, isInfix, funcExpr, argExpr, range) ->
    match funcExpr with
      // ...

  | _ ->
    None  // (None is default visiting)

Which choice of better technics? There is both merits and demerits which is desirable. If you use OOP based, you can receive all AST nodes at visit timing but more complex and difficult for how to identicates structurable-nested AST nodes. If you use FP based, can easier structurable-nested AST nodes (by pattern matching) but you can receive only “SynExpr” nodes at visit timing.

These helpers are totally not depended on fscx. I separate visiter pattern helpers from fscx and construct standalone package for “FSharp.Compiler.Service.Visitors.” If you are thinking about how to apply visitor pattern for your application, try to use this package.

# This package fixed for the most part in version 0.7.1.
# Maybe not change for public declarations.


What difficults for AST translations?

The AST translation code has cool things (Maybe ;)

But AST translation always most difficult technics in real story. You are the man of always designing for F# Compiler Service/Roslyn’s AST everydays :) So you are understand it’s really difficult for meaning and knowning. F# ASTs are likely Expression-tree classes, but translation is difficult more than ExpressionVisitor experience…

# If you want more informations about Expression-tree, read these posts: Second half of Advent LINQ 2013 (In Japanese) and/or “Final LINQ Extensions III” (In Japanese).

We are trying to implement the “sample-filter” above: We need knowns about what realize AST structures and where AST nodes appearance for? And how to translate AST nodes?

AST nodes constructs for recursivility. So if you identicate AST nodes by rough technics, results are broken easier. (OK, F# Compiler Service find invalid AST structures and report errors at compile time. The world maybe safe :)

This example illustlated for how different AST structures on source code by small different:

// The standard .NET method signature (Tupled arguments)
let output1 (a: int, b: string, c: double) =
  System.Console.WriteLine("output1: {0}:{1}:{2}", a, b, c)

// F# function (Curryable arguments)
let output2 (a: int) (b: string) (c: double) =
  System.Console.WriteLine("output2: {0}:{1}:{2}", a, b, c)

“output1” method is most popular standard .NET method. “output2” function is F# friendly the “Function.” If you write to call these members:

// Call the standard .NET method signature (Tupled arguments)
// (int * string * double) -> unit
output1 (123, "ABC", 456.789)

// Call the F# function (Curryable arguments)
// int -> string -> double -> unit
output2 123 "ABC" 456.789

The “Tupled arguments” method different for “Curryable arguments” function. We are finding for differents in AST nodes. Tupled arguments realize for SynExpr.Tuple AST node. But curryable arguments realize for nested SynExpr.App AST nodes.

It’s difficult that different on ASTs. I want to see AST structures. So I make smaller tool for dump AST nodes by F# Compiler Service (This tool for smaller hacks, very rough quality.)

This is output (Only contains interested fragments):

// For output1 method:
Ast.SynExpr.App( (* expr1 *)          // App - output1(...)
  ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
  false (* isInfix *),
  Ast.SynExpr.Ident( (* funcExpr *)
    output1 (* Item *)),
  Ast.SynExpr.Paren( (* argExpr *)    // Paren --> Tuple : Tupled arguments
    Ast.SynExpr.Tuple( (* expr *)
      [ (* exprs *)                   // Arguments declaring by list
        Ast.SynExpr.Const( (* [0] *)
          Ast.SynConst.Int32( (* constant *)
            123 (* Item *)));
        Ast.SynExpr.Const( (* [1] *)
          Ast.SynConst.String( (* constant *)
            "ABC" (* text *)));
        Ast.SynExpr.Const( (* [2] *)
          Ast.SynConst.Double( (* constant *)
            456.789 (* Item *)))],
      [ (* commaRanges *)]),
    range>.Some( (* rightParenRange *)))),

// For output2 function:
Ast.SynExpr.App( (* expr2 *)          // App - Apply 456.789
  ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
  false (* isInfix *),
  Ast.SynExpr.App( (* funcExpr *)     // App - Apply "ABC"
    ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
    false (* isInfix *),
    Ast.SynExpr.App( (* funcExpr *)   // App - Apply 123 for output2
      ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
      false (* isInfix *),
      Ast.SynExpr.Ident( (* funcExpr *)
        output2 (* Item *)),
      Ast.SynExpr.Const( (* argExpr *)
        Ast.SynConst.Int32( (* constant *)
          123 (* Item *)))),
    Ast.SynExpr.Const( (* argExpr *)
      Ast.SynConst.String( (* constant *)
        "ABC" (* text *)))),
  Ast.SynExpr.Const( (* argExpr *)
    Ast.SynConst.Double( (* constant *)
      456.789 (* Item *))))),

It’s contains too many verbose fragments so ugly (sorry). That means output1’s arguments declarates by list, output2’s arguments declarates by nested “SynExpr.App” (Apply argument).

That is the “Curryable arguments,” means:

// Call the F# function (Curryable arguments)
// int -> string -> double -> unit
output2 123 "ABC" 456.789

// It's meaning for expression:
((output2 123) "ABC") 456.789

// Split expressions:
let f1 = output2 123  // f1 is higher order function
let f2 = f1 "ABC"     // f2 is higher order function
f2 456.789

The AST structure different for smaller on original source codes. So we are sensitive for analyse by visitor pattern. This is just difficult…

# I have an idea from these things. I have a session for “You will be assimilated. Resistance is futile.” on “NGK2016B” LT conference (This is joke session :)


The filter’s middleware

So we can do anything by filter library AND implementation are very hard work. Then I’m thinking about how to do easier it…
I’m trying to design the helper library: “FSharp.Expandable.Compiler.Aspect.”

Will we have what intercept anything (by fscx)? This library is “Middleware” for fscx, we can safe implements for insert custom code between enter and leave function points. Today, we are calling the “AOP (Aspect oriented paradigm)” for this technics.

If we use this helper library by develop filter library in this case, our filter code are simple, safe and more easier.

For example below:

// This is "Aspect context" class
type SampleAspectContext internal (body: string) =
  // Finish aspect (trigger are leaved method with return value)
  member __.Leave(result: 'T) : 'T =
    Console.WriteLine("Leave: " + body)
    result
  // Finish aspect (trigger are leaved method with exception)
  member __.Caught(ex: exn) : unit =
    Console.WriteLine("Caught: " + body + ": " + ex.ToString())

// This is "Aspect" class
type SampleAspect() =
  // Start aspect (trigger are entered method)
  static member Enter(methodName: string, fileName: string, line: int, column: int, args: obj[]) =
    let body =
      String.Format
        ("{0}({1}, {2}): {3}({4})",
         fileName,
         line,
         column,
         methodName,
         String.Join(", ", args |> Seq.map (sprintf "%A")))
    Console.WriteLine("Enter: " + body)
    new SampleAspectContext(body)

This sample codes (Called “Aspect code”) are printing to console for function call message between enter and leave (include caught exception). “SampleAspect.Enter(…)” method is calling from before function call and “SampleAspectContext.Leave(…)” or “Caught(…)”
methods are calling from after function called.

“Enter(…)” method received the method name, source code file name, line/column number and will be calling with arguments. This sample codes prints these information to Console and create instance for “SampleAspectContext” class and return.

“Leave(…)” or “Caught(…)” handles to print leave messages. “Leave(…)” can receive for function called result. “Caught(…)” can receive for exception instance.

These aspect codes are truly “Duck-typed.” fscx will be identicate and not require any base classes and any interfaces. Because if request these types, we have .NET assembly referencing problem and fix this problem for very hard on packaging by NuGet :(

Ready for aspect named “SampleAspect” class. Next step, how to identicate this aspect for “FSharp.Expandable.Compiler.Aspect?” :

// This is declaring for aspect: "SampleAspect" class.
// Class name is free.
[<Sealed>]
type SampleAspectLoggerDeclaration() =
  // Construct with aspect class name. This is duck-typed naming (by string.)
  inherit DeclareFscxInjectAspectVisitor("SampleAspectLogger.SampleAspect")

Totally aspect tips: Aspect codes and this declaration requests loose types, so we can write aspects by C#.

This declaration contains on filter library, fscx can find and translate:

  • If fscx is targeting for a function, fscx insert custom AST nodes before calling function. Call to “SampleAspect.Enter(…)”
  • And fscx insert custom AST nodes after called function. Call to “SampleAspectContext.Leave(…)” or “Caught(…).” Leave with called function result value, Caught with exception instance by (inserted) try-with block.

This sample code fragment:

let f11 (a: int, b: string, c: int) =
  output1(a + c, b, 123.456)

Translate to:

let f11 (a: int, b: string, c: int) =
  let __arg_0 = a + c
  let __arg_1 = b
  let __arg_2 = 123.456
  let __context =
    SampleAspectLogger.SampleAspect.Enter
      ("f11", "SampleCode.fs", 2, 3, [|__arg_0, __arg_1, __arg_2|])
  try
    __context.Leave(output1(__arg_0, __arg_1, __arg_2))
  with
  | ex ->
    __context.Caught(ex)
    reraise()

This sample code declare “f11(…)” by tupled arguments method, sure can translate for function by curryable arguments.

Totally at this point: We have only the aspect classes “SampleAspect, SampleAspectContext” and aspect declaration “SampleAspectLoggerDeclaration”. These are contains no AST translation codes… Oh I made it! :)

Currently, I’m developing “FSharp.Expandable.Compiler.Aspect” now and not finish, because how to set where insert aspects to the target function.


Conclusion

fscx is challenging project for me. This project has several points:

  • fscx is easy-usage (by install NuGet) for any filter users.
  • We can develop any auto translation by ASTs (but very difficult).
  • We can develop easier any AOP based senarios by using fscx’s middleware.
  • All filter libraries can deploy by NuGet packaged.

The myth: We write OOP based codes by F# that’s hassle. Because if we become to FP, a little bit tired for declare many class and/or interface word elements. (It’s myth! don’t make seriously :)

We often write for UI codes, for example how to implement “INotifyPropertyChanged.” fscx is capable for can filtering auto-implement this interface, these senarios capsule to the middleware and running this code by no runtime cost. Finally we can construct ecosystem for filter libraries.

Acknowledgments: Thanks cooperation for On the road corporation and Mr. bleis.

Thank you for reading, welcome any comments! Happy christmas!!