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

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

英語版はこちら (For English post: http://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: (http://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!!

パターンマッチングの面白さを見る

fsharpツイッターのTL見ていると、徐々にC# 7の話題が出てきています。C# 7でパターンマッチングが導入されるとか(まだ範囲は確定してません)。そんなわけで、「パターンマッチング」で面白いと感じた事を書いてみます。

C# 7で導入が検討されている機能については、岩永さんの記事が非常にわかりやすくて良いと思います。是非参照してみてください。

私の場合、時期が微妙にずれているのですが、C# 7ではなくF#でパターンマッチングに触れました。その前にはScalaでほんの少しだけ触れたのですが、結局実用的なコードをほとんど書かなかったこともあって、モノにもならなければその良さもわからず仕舞いでした。さらにその前では、某名古屋のシャチョーさんにHaxeで判別共用体を使う例をちょっとだけ紹介してもらった事がある程度です。

F#は業務で使っていることもあり(但し、まだ使いこなしているとは言い難い)、F#に限ってですが、ようやくその利点が頭の中で整理されてきた感があります。「パターンマッチング」の何が良いのか、面白いのか、という事を明らかに出来たらいいなーと思っています。

# サンプルコードはそのままではコンパイルできない式があったりしますが、説明を優先します。また、F#らしからぬコード(例: printfn使ってない)がありますが、対比を優先させています。加えて、多少冗長に書いている箇所があります。


switch-caseの代替としてのパターンマッチング

パターンマッチングの最初の導入は、単にif-then-elseやswitch-caseの置き換えと言うものです。その前にswitch-caseの導入を(ばかばかしいですが)しておきましょう。

var value = 102;

// ifで分岐
if (value == 100)
{
  // 100に対応する処理
}
else if (value == 101)
{
  // 101に対応する処理
}
else if (value == 102)
{
  // 102に対応する処理
}
else if (value == 103)
{
  // 103に対応する処理
}
else
{
  // それ以外の値の処理
}

これが:

var value = 102;

// switchで分岐
switch (value)
{
  case 100:
    // 100に対応する処理
    break;
  case 101:
    // 101に対応する処理
    break;
  case 102:
    // 102に対応する処理
    break;
  case 103:
    // 103に対応する処理
    break;
  default:
    // それ以外の値の処理
    break;
}

こうなりますね。利点としては、判定される対応する値が「case」句によって明示される(のでわかりやすい)という事でしょうか。また、case句には定数しか置けないので、複雑な判定条件が入る余地がないという事もあります。私はこれを「宣言的」だと感じています。デメリットとして、それがそのまま欠点となり、caseに判定式が書けないという事になります。

C# 1(1ですよ)が出た当時、Javaとの比較において、判定対象に文字列が使えることがアドバンテージだと言われていました。

var value = "CCC";

// switchで分岐
switch (value)
{
  case "AAA":
    // "AAA"に対応する処理
    break;
  case "BBB":
    // "BBB"に対応する処理
    break;
  case "CCC":
    // "CCC"に対応する処理
    break;
  case "DDD":
    // "DDD"に対応する処理
    break;
  default:
    // それ以外の値の処理
    break;
}

私は文字列をswitch-caseの判定に使えるからと言って、それが大きな利点だとは思えませんでした。むしろ、判定対象の値がプリミティブ型と文字列だけではなく、それ以外の型のインスタンスが指定されたら、EqualsやIEquatable<T>を使って判定してくれればもっと使い出があるのに、と思ってました(caseにどのようにインスタンスを指定するのかという課題はありますが…)。このあたりのモヤモヤにも、あとで取り組みます。

さて、switch-caseと同じことをF#のパターンマッチングでやってみます。

let value = 102

// matchでパターンマッチング
match value with
| 100 ->
  // 100に対応する処理
| 101 ->
  // 101に対応する処理
| 102 ->
  // 102に対応する処理
| 103 ->
  // 103に対応する処理
| _ ->
  // それ以外の値の処理

F#を書いたことがない人でもだいたい読めると思います。switch-caseと比較してもほぼ一対一に置き換わっている感じがしますね。

最初のポイントは、match-withを使う場合は「それ以外の処理」を省略できない事です。F#ではすべての式(上記では処理)が戻り値を持つことになっているので、特定のマッチが値を返さないと矛盾してしまいます。最後のアンダースコアを使う “_ -> …” というマッチで、その他の値が担保されます。これによって、valueに対する評価が「網羅される」(条件に抜けがない)ことがコンパイラによって保証されます(漏れていると警告が発生します。目的を考えるとバグと思われるので、警告をエラーとするコンパイルスイッチを有効化しても良いかもしれません)。


直値を避けることと、その判定

上記に挙げたような「直値」による判定は、出来るだけ避けた方が良いというのが一般的な指針でしょう。簡単なミスですが、例えば”100″とタイプするところを”10″とタイプしてしまってこれに気が付かないというようなバグを生んでしまう可能性があります。C/C++言語であれば、#defineによるプリプロセッサを使って置き換えることを考えると思います。或いは(C#でも)enum型をつかえば、直値をシンボリックな値に置き換える事が出来ます。そして、enum値はswitch-caseでも使えます。

# 抽象型を導入して解決すべきという意見もあるかもしれませんが、その話はまた後で。

// enum型を宣言する
public enum Modes
{
  LightMode = 100,
  StandardMode = 101,
  ManualMode = 102,
  CatMode = 103
}

var value = Modes.StandardMode;

// switchで分岐
switch (value)
{
  case Modes.LightMode:
    // LightModeに対応する処理
    break;
  case Modes.StandardMode:
    // StandardModeに対応する処理
    break;
  case Modes.ManualMode:
    // ManualModeに対応する処理
    break;
  case Modes.CatMode:
    // CatModeに対応する処理
    break;
}

良い感じです。普段はシンボル名を使うとして、実際の直値”100″をLightModeに対応付ける方法は考える必要があります。C#の場合は、上記のようにenumの各値にintの値を対応付けることもできるので、これを使うと実装が楽になると思います。

同じようにこれをF#で書きます:

// enum型を宣言する(数値は省略)
type Modes = LightMode | StandardMode | ManualMode | CatMode

let value = Modes.StandardMode

// matchでパターンマッチング
match value with
| Modes.LightMode ->
  // LightModeに対応する処理
| Modes.StandardMode ->
  // StandardModeに対応する処理
| Modes.ManualMode ->
  // ManualModeに対応する処理
| Modes.CatMode ->
  // CatModeに対応する処理

match-with式が値の網羅性を担保するため、enum型の値4つのパターンが網羅されていればコンパイルに成功します。逆に足りない場合は警告が発生するので、うっかり記述漏れを起こすということがありません。逆に「該当しない」場合を泥縄的に処理する場合は、defaultと同じように書く事が出来ます:

match value with
| Modes.LightMode ->
  // LightModeに対応する処理
| Modes.CatMode ->
  // CatModeに対応する処理
| _ ->
  // それ以外の値をすべて処理

F#が、式が完全である事を要求するこだわりは、以下の例でよくわかります。

// enum型を宣言する(対応する数値を明示する)
type Modes = LightMode = 100 | StandardMode = 101 | ManualMode = 102 | CatMode = 103

let value = Modes.StandardMode

// "警告 FS0025: この式のパターン マッチが不完全です
//  たとえば、値 'enum<Modes> (0)' はパターンに含まれないケースを示す可能性があります。"
match value with
| Modes.LightMode ->
  // LightModeに対応する処理
| Modes.StandardMode ->
  // StandardModeに対応する処理
| Modes.ManualMode->
  // ManualModeに対応する処理
| Modes.CatMode->
  // CatModeに対応する処理

まず、enum型の値に数値を明示する場合は、すべての値に明示する必要があります(C#では省略可能で、直前の値がインクリメントされる)。そして、上記のenum値には”0″に対応する値が定義されていないため、「valueが値”0″に相当した場合の処理を網羅していない」という警告が発生します。

詳しい説明は省きますが、enum型はValue-Type(値型)なので、デフォルトの値(C#で言う所のdefault(Modes))となる可能性があり、ここではそのことを警告しています(言語というより、.NET CLRの特性による)。enum型の値に対応する数値を明示的に指定しない場合、enum型の最初の値(上記ではLightMode)が値”0″に対応するので、結果的に警告は発生しません。


switch-caseとパターンマッチングの分かれ目

前節の例のように、F#は「転びそうなミスを拾ってくれ」ます。しかし、今まで見てきた例は、殆ど直接的にif-then-elseやswitch-caseに置き換える事が出来ます。これでは、パターンマッチングという新しい武器に「興味をそそられない」としても、仕方が無いと思います。

しかし、ここからがパターンマッチングの強力さを発揮する部分です。

複数の値の組み合わせに基づいて処理を決定するようなコードを書いたことがあるでしょう。ここにswitch-caseを適用すべきかどうかは、個人によって判断の基準が色々ありそうです。ここではベタに書いてみます。

// enum型の定義は省略
var value1 = Modes1.StandardMode;  // (4種類)
var value2 = Modes2.PowerfulMode;  // (3種類)

switch (value1)
{
  case Modes1.LightMode:
    switch (value2)
    {
      case Modes2.MinimumMode:
        // LightModeかつMinimumModeの処理
        break;
      case Modes2.RichMode:
        // LightModeかつRichModeの処理
        break;
      case Modes2.PowerfulMode:
        // LightModeかつPowerfulModeの処理
        break;
    }
    break;

  case Modes2.StandardMode:
    switch (value2)
    {
      case Modes2.MinimumMode:
        // StandardModeかつMinimumModeの処理
        break;
      case Modes2.RichMode:
        // StandardModeかつRichModeの処理
        break;
      case Modes2.PowerfulMode:
        // StandardModeかつPowerfulModeの処理
        break;
    }
    break;

  // (以下、ひたすら続く...)
}

「こんな惨いコード書かねぇ!これならifで書いたほうが良い!!」或いは「抽象化しろ!」って言われそうです。見た目の問題やコードがやたら長くなることも問題ですが、もっと深刻なのは、組み合わせが正しく網羅されているのかどうかを確認するのが大変そうだという事です。

F#で書いてみます:

// enum型の定義は省略
let value1 = Modes1.StandardMode;  // (4種類)
let value2 = Modes2.PowerfulMode;  // (3種類)

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
  // LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
  // LightModeかつPowerfulModeの処理

| Modes1.StandardMode, Modes2.MinimumMode ->
  // StandardModeかつMinimumModeの処理
| Modes1.StandardMode, Modes2.RichMode ->
  // StandardModeかつRichModeの処理
| Modes1.StandardMode, Modes2.PowerfulMode ->
  // StandardModeかつPowerfulModeの処理

| ... (以下、マッチ式が続くが、組み合わせが網羅されてないと警告が出る)

私は初見で、C#にもこれ欲しい!!と思いました。或いは、これならネストしていた方が良いと思う人もいるかもしれません(matchをネストさせることもできますが省略)。

しかし、パターンマッチングはもっともっと強力です。面白いのは、組み合わせ網羅を全部書かなくても良いことです:

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
  // LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
  // LightModeかつPowerfulModeの処理

| _, Modes2.MinimumMode ->
  // 残りのMinimumModeの処理
| _, Modes2.RichMode ->
  // 残りのRichModeの処理
| _, Modes2.PowerfulMode ->
  // 残りのPowerfulModeの処理

// (これですべて網羅された)

これはつまり、value1がLightModeの時だけ、value2に応じた個別の処理を行い、value1がそれ以外の値の場合はvalue2に応じた共通かつ個別の処理を行うという事です。これを見て、コードがより「宣言的」に近づいたと思いませんか? 実際、F#コンパイラはこれらの網羅に漏れがないかどうかをチェックしています。さらに複雑な例を見てみます:

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
  // LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
  // LightModeかつPowerfulModeの処理

| Modes1.CatMode, Modes2.RichMode ->
  // CatModeかつRichModeの処理

| _, Modes2.MinimumMode ->
  // 残りのMinimumModeの処理
| _, Modes2.PowerfulMode ->
  // 残りのPowerfulModeの処理

| _, _ ->
  // 残りのすべての処理

「残りのすべての処理」が無いと警告が発生します。何が該当するかは、実際に考えてみて下さい。

もちろん、match式の組み合わせ対象の値は、更に3個4個…n個とカンマで区切って指定できます。複雑にネストしてしまうようなif-thenやswitch-caseよりもスマートに書ける上に、メタプログラミング(T4のようなコード生成)でmatch式を動的に生成する場合にも重宝しそうです。


タプルとパターンマッチングの関係

前述の例で、Modes1の残りの値の組をデフォルト処理しましたが、具体的に値が何であったのかを知る方法があります。

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // LightModeかつMinimumModeの処理
| Modes1.LightMode, Modes2.RichMode ->
  // LightModeかつRichModeの処理
| Modes1.LightMode, Modes2.PowerfulMode ->
  // LightModeかつPowerfulModeの処理

| v1, Modes2.MinimumMode ->
  // (v1からModes1の実際の値が得られる)

| _, _ ->
  // 残りのすべての処理

アンダースコアの代わりにシンボル(ここではv1)を指定することで、対応する値が何であったのかを得る事が出来ます。value2はマッチ式からMinimumModeであることが分かっているので、value2については不要ですね。この例ではv1とはつまりvalue1の事なので、値が取得できたところで意味が無い(value1でアクセスすればよい)ように見えます。この構文が生かされるのはもう少し後です。

所で、マッチ式を以下のようにして、結果をまとめて取得する方法もあります。

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
| Modes1.LightMode, Modes2.MinimumMode ->
  // ...
| Modes1.LightMode, Modes2.RichMode ->
  // ...
| Modes1.LightMode, Modes2.PowerfulMode ->
  // ...

| _, _ as pair ->
  // (pairで両方の値にアクセスできる)

「as」演算子を使うと、パターンマッチングの結果が参照できるようになります。では、上記の「pair」で何が参照出来るのでしょうか? 実はpairは「タプル」になります。F#のタプルは、内部的にはSystem.Tuple<…>で定義されているのですが、Item1、Item2のようなプロパティは参照できず、その必要もありません。

タプルの値を分解するには、以下のようにletを使います:

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
  // ...

| _, _ as pair ->
  let v1, v2 = pair  // v1とv2にそれぞれItem1とItem2の値が入る
  System.Console.WriteLine("{0}, {1}", v1, v2)  // 普通に使える

v1とv2はもちろん型推論で自動的にModes1型とModes2型として扱われます。ところでこの式を見ていて、マッチ式がモヤモヤしてきませんか?

// 組み合わせをパターンマッチングで評価する
match value1, value2 with
  // ...

| v1, v2 ->  // letで分解しなくても、実はこれでいい?
  // let v1, v2 = pair  // v1とv2にそれぞれItem1とItem2の値が入る
  System.Console.WriteLine("{0}, {1}", v1, v2)

そして:

// タプルを作る
let pair = (value1, value2)

// それって実はこうじゃね?
match pair with
| Modes1.LightMode, Modes2.MinimumMode ->
  // ...
| Modes1.LightMode, Modes2.RichMode ->
  // ...
| Modes1.LightMode, Modes2.PowerfulMode ->
  // ...

| v1, v2 ->  // letで分解しなくても、実はこれでいい?
  // let v1, v2 = pair  // v1とv2にそれぞれItem1とItem2の値が入る
  System.Console.WriteLine("{0}, {1}", v1, v2)

実は “match value1, value2 with” という式は、組み合わせマッチングのための特別な構文ではなく、その場でvalue1とvalue2のタプルを作って渡しているだけと見る事が出来ます。更にマッチ式のカンマで区切った構文も特別な構文ではなく、タプルの各要素にマッチングしているだけなのです!!

そういうわけで、パターンマッチングを書いている場合は、実はあまりタプルの事は意識していませんが、内部では非常に多くの場面でタプルが自動的に使われています。

この例でもまだvalue1とvalue2にはアクセスできるので、v1、v2に分解する事に意味を見いだせないかもしれません。これならどうでしょうか:

// 関数化(引数はタプル)
let applyPair p =
  // タプルから直接パターンマッチングする
  match p with
  | Modes1.LightMode, Modes2.MinimumMode ->
    // ...
  | Modes1.LightMode, Modes2.RichMode ->
    // ...
  | Modes1.LightMode, Modes2.PowerfulMode ->
    // ...
  | v1, v2 ->  // マッチ式で直接タプルを分解
    System.Console.WriteLine("{0}, {1}", v1, v2)

// タプルを作る
let pair = (Modes1.StandardMode, Modes2.PowerfulMode)

// 関数呼び出し
applyPair pair

もはやapplyPairは独立した関数なので、内部のマッチ式で直接value1、value2にはアクセスできません。それどころかタプルを直接生成しているので、value1もvalue2も定義されていません。

関数の引数pは型が推測され、Modes1とModes2のタプルとなります。そしてマッチ式によって判定とタプルの分解が一度に行われます。これに慣れてくると、Tuple<…>ほにゃほにゃとか、Item1、Item2のようにプロパティにいちいちアクセスするのが、とても面倒になってきます。また、C#でTupleを使うと意味が不明瞭になるため乱用しない方が良いというセオリーがありますが、F#のタプルではその点で困ることはあまりありません。


シーケンスのマッチング

シーケンスとは、IEnumerable<T>の事です。つまりC#で言う所のLINQ可能なインスタンスです。F#の世界では、手軽に扱えるシーケンスが2種類あります。「リスト」と「配列」です。F#配列は、System.Arrayの派生クラスなのでC#と同じですが、F#のリストは.NETの「System.Collections.Generic.List<T>」クラスではなく、読み取り専用の順方向リンクリストとして実装されたクラスです。これを使うと、シーケンスの各要素をパターンマッチングでマッチさせながら分解までやらせることが出来ます。

// F#リストを手動で定義
let values = [102; 107; 111; 133; 50]

// F#リストをパターンマッチングする
match values with
| [] ->
  // 空のリストにマッチ
  System.Console.WriteLine("Empty")

| [x; y; z] ->
  // 3つの要素が含まれるリストにマッチし、かつそれぞれの値を参照可能にする
  System.Console.WriteLine(
    System.String.Format(
      "x={0},y={1},z={2}",
      x, y, z))

| [a; b; c; d; e] ->
  // 5つの要素が含まれるリストにマッチし、かつそれぞれの値を参照可能にする
  System.Console.WriteLine(
    System.String.Format(
      "a={0},b={1},c={2},d={3},e={4}",
      a, b, c, d, e))

| xs ->
  // その他のリストにマッチし、xsで参照可能にする
  System.Console.WriteLine(System.String.Join(",", xs))

この例では、a,b,c,d,eの5つの要素を分解するマッチ式にマッチします。これもベタで書くと面倒なコードになりますが、パターンマッチングで書けばシンプルかつ安全に書くことが出来ます。

配列の構文は “[| … |]” ですが、上記のF#リストの例と全く同じように記述できます。残念ながら「シーケンスそのもの」をパターンマッチングでマッチさせる事はできません。シーケンスを扱う場合は、リストか配列に固定化すれば扱えます。

// F#リストを手動で定義し、シーケンスとして扱う
// 型: seq int (IEnumerable<int>)
let values = seq [102; 107; 111; 133; 50]

// シーケンスをF#リストに変換してマッチさせる
match values1 |> Seq.toList with
| [] ->
  // ...
| [x; y; z] ->
  // ...
| [a; b; c; d; e] ->
  // ...
| xs ->
  // ...

ところで、パターンマッチングで使える道具はほとんど自由に組み合わせて使えます。enum型も使えるし、タプルでもOKです。それらをマッチ式内で分解させると、普通に書くには面倒すぎる操作も自由自在です。組み合わせた例を示します:

// enum型を宣言する
type Modes = LightMode | StandardMode | ManualMode | CatMode

// 複雑なF#リストを定義する
let values = [
  (102, ("ABC", Modes.StandardMode));
  (107, ("DEF", Modes.CatMode));
  (111, ("GHI", Modes.LightMode));
  (133, ("JKL", Modes.StandardMode));
  (50, ("MNO", Modes.ManualMode))
]

// F#リストを要素毎に定義されたマッチ式でマッチさせる
match values with
| [] ->
  System.Console.WriteLine("Empty")

| [(number0, (name0, mode0)); (number1, (name1, mode1))] ->
  // 2つの要素が含まれるリストにマッチし、内容をすべて分解して参照可能にする
  System.Console.WriteLine(
    System.String.Format(
      "Number={0},Name={1},Mode={2}",
      number0, name0, mode0))
  System.Console.WriteLine(
    System.String.Format(
      "Number={0},Name={1},Mode={2}",
      number1, name1, mode1))

| [(number0, (name0, mode0)); _; _; _; (number1, (name1, mode1))] ->
  // 5つの要素が含まれるリストにマッチし、最初と最後の要素だけを分解して参照可能にする
  System.Console.WriteLine(
    System.String.Format(
      "Number={0},Name={1},Mode={2}",
      number0, name0, mode0))
  System.Console.WriteLine(
    System.String.Format(
      "Number={0},Name={1},Mode={2}",
      number1, name1, mode1))

| _ ->
  // ...

マッチする事自体は判定したいけど、実際にマッチした値が不要なら、アンダースコアを使って取得しない、という選択も出来ますよ。そしてもちろん、タプルの内側の特定の値だけ不要な場合とか、全く同じように書けます(例えばname1が不要なら、”(number1, (_, model1))”)。

「うわ、何これ…」みたいに感じてきましたか? まだまだこれからです。


実行時の型でマッチングする

例えば、与えられる値の種類に応じて、それぞれの処理を切り替えたいという要求はよくあると思います。XML DOMのノードが良い例です。XMLのノードは、エレメント・テキスト・XML属性などで構成されていますが、トラバースする場合は同じ「ノード」として扱い、それぞれの場合に応じて処理を変更する必要があります。

using System.Xml.Linq;

// 指定されたノードを解析して結果を取得する再帰メソッド
public static string TraverseNode(XNode node)
{
  // ノードがエレメントなら
  var element = node as XElement;
  if (element != null)
  {
    // 子ノードも探索する
    return string.Format(
      "Element: {0} {{ {1} }}",
      element.Name,
      string.Join(", ", element.Elements().Select(child => TraverseNode(child))));
  }

  // ノードがテキストなら
  var text = node as XText;
  if (text != null)
  {
    return string.Format(
      "Text: \"{0}\"",
      text.Value);
  }

  // (他は省略)
  return "(Unknown)";
}

ここで、switch-caseが使えれば良いのですが、caseには直値を指定しなければならないため、switch-caseは使えません。同じような処理をF#で書いてみます:

open System.Xml.Linq

// 指定されたノードを解析して結果を取得する再帰関数(引数に型注釈が必要)
let rec traverseNode (node: XNode) =
  match node with
  // ノードがエレメントなら
  | :? XElement as element ->
    System.String.Format(
      "Element: {0} {{ {1} }}",
      element.Name,
      System.String.Join(", ", (element.Elements() |> Seq.map (fun child -> traverseNode child))))

  // ノードがテキストなら
  | :? XText as text ->
    System.String.Format(
      "Text: \"{0}\"",
      text.Value)

  // (他は省略)
  | _ -> "Unknown"

# 型注釈と言うのは、型を推測できない場合にヒントとして補うことです。この例で型注釈(XNode)が必要な理由については次節で説明します。

マッチ式が “:? XElement as element” となっていますが、これは「指定された値の型がXElementでキャスト可能な場合に、キャストされた値を”element”で参照できるようにする」という意味です。XTextも同様です。このように、switch-caseのような直値だけではなく、柔軟な判定を行う事が出来ます。

マッチ式に “:?” パターンを使う場合は、必ずすべてのマッチ式が “:?” を使わなければならない訳ではありません。有効なマッチ式であればどのようにでも指定する事が出来ます。


言語機能の直交性

前節の例では、.NET標準のXML LINQの型を使ったので、型の動的な判定を行わせています。XElementやXTextはXNodeを継承しているという「特性を利用」し、引数ではXNodeを渡させています。引数にXNodeの型注釈が必要なのは、クラスの継承やインターフェイスの実装を考えると、F#だけで型を推測することが出来ないためです(未知の関連型を網羅検索出来る必要がある)。

そのため、F#だけで書いてよいのなら、OOP的なアプローチを取らず、「判別共用体」という種類の型を使います。

// ノード型(判別共用体)
type Node =
  // エレメントの場合は名前と子要素のシーケンスをタプルで保持
  | Element of (string * (Node seq))

  // テキストの場合は文字列を保持
  | Text of string

# タプルの型記法は慣れてないと気持ち悪いかもしれません “(type1 * type2 …)” のようにアスタリスクで要素の型を区切ります。

「共用体」というと、C言語のunionを思い浮かべる人も居るかもしれませんが、意図としては似ています。このコードは”Node”型を定義しますが、これはいわゆる「基底クラス」や「基底インターフェイス」ではありません。内部に定義されている「Element」と「Text」の二つの種類の値(用語でTagと言います)を持ちうる型です。また、この値以外の値が存在しない(未知の値がない)事がはっきりしています。

enum型と比較すると、シンボル名が整数に対応づけされるものではなく、それぞれの値が独自の異なる値の組を持つ事が出来るところが違います。

  • Nodeの値が”Element”である場合は “(string * (Node seq))” つまり文字列とNodeのシーケンス(のタプル)を持つ。
  • Nodeの値が”Text”である場合は “string” つまり文字列だけを持つ。

# 判別共用体がIL上でどのように実現されているのかについては、ここでは触れません。少なくともILのネイティブ表現に対応するものはありません。

C言語のunionは危険な使い方が出来ますが、判別共用体で危険な使い方は出来ません。例えば、Nodeの値が”Text”であるのに、無理矢理Elementとみなしてアクセスすることは不可能です。逆もまた然り。その安全性の担保は、マッチ式で行われます。

type Node =
  | Element of (string * (Node seq))
  | Text of string

// 指定されたノードを解析して結果を取得する再帰関数
let rec traverseNode node =
  match node with
  // ノードがエレメントなら
  | Element (name, children) ->
    System.String.Format(
      "Element: {0} {{ {1} }}",
      name,
      System.String.Join(", ", (children |> Seq.map (fun child -> traverseNode child))))

  // ノードがテキストなら
  | Text text ->
    System.String.Format(
      "Text: \"{0}\"",

  // (他の値はとりようがないのですべて網羅された)

このコードには色々と「おいしい」部分が含まれています。まず、関数の引数に型注釈は不要です。どうやってNode型であることを推測しているかというと、マッチ式に”Element”と”Text”が現れ、しかもキャプチャする引数の並びがすべて一致(Elementは2個、Textは1個)し、それぞれの引数の型が、内側の式から推測された型に一致する定義がNode型しか無いと認識できるからです。

そしてマッチ式を見ると、ElementとTextに「関数引数のようなもの」が指定されています。これらが、それぞれの値(Tag)にマッチした時の値として参照可能になります。つまり:

type Node =
  | Element of (string * (Node seq))  // (name, children)
  | Text of string                    // text

let rec traverseNode node =
  match node with
  | Element (name, children) ->
    // 判別共用体Element Tagのタプルにマッチし、
    // タプルの値がそれぞれ name と children で参照可能となるように分解される

  | Text text ->
    // 判別共用体Text Tagにマッチし、文字列がtextで参照可能になる

前節でタプルをパターンマッチングで分解する例を見せましたが、ここでも分解が応用されています。もちろん:

let rec traverseNode node =
  match node with
  | Element pair ->
    // 判別共用体Element Tagのタプルにマッチし、pairで参照可能になる
    let name, children = pair
    // ...

  | Text text ->
    // 判別共用体Text Tagにマッチし、文字列がtextで参照可能になる

タプルをマッチ式で分解しないで、直接参照可能にすることもできます。

このように、パターンマッチングで使われるこまごまとした機能が直交的に組み合わされて、高い表現力を持つようになっています。前節で示した、enum型・タプルのネスト・シーケンスのマッチングを判別共用体と組み合わせることも可能です。


ネストした判別共用体のマッチング

パターンマッチの機能が直交的だと思える例をもう一つ。ここまでで示した道具がすべて組み合わせ可能と言う事は、判別共用体自身もまた組み合わせ可能と言う事です。ネストした判別共用体の強力なパターンマッチング例をお見せします。

type Value =
  | Numeric of int
  | Text of string
  | Date of System.DateTime

// ネストした判別共用体
type Information =
  | Type1 of (string * Value)
  | Type2 of string

// Information型の値
let value = Type1 ("ABC", Numeric(123))

// Information型のパターンマッチング
match value with
| Type1 (name, Numeric numeric) ->
  System.Console.WriteLine(
    "Type1: Name={0}, Numeric={1}",
    name,
    numeric)

| Type1 (name, Text text) ->
  System.Console.WriteLine(
    "Type1: Name={0}, Text={1}, Length={2}",
    name,
    text,
    text.Length)

| Type1 (name, Date date) ->
  System.Console.WriteLine(
    "Type1: Name={0}, Year={1}, Month={2}, Day={3}",
    name,
    date.Year,
    date.Month,
    date.Day)

| Type2 text ->
  System.Console.WriteLine(
    "Type2: Text={0}",
    text)

もちろん、いくらでもネスト可能で、ネストした細部の値を直接分解して参照することが出来ます。パターンマッチング無しで同じ処理を実現しようと思うと、不可能ではありませんが(大した処理ではないにも関わらず)非常に面倒でミスも起きやすいコードになると思います。


型のメンバーにパターンマッチングしたい

ここまでマッチングが柔軟であると、クラスのメンバ、例えばプロパティに対してもパターンマッチングしたいと考えるかもしれません。

// クラス型の定義(引数はコンストラクタの引数)
type DemoClass (firstName: string, lastName: string, age: int) =
  // コンストラクタ引数の値を読み取り専用プロパティとして公開
  member __.FirstName = firstName
  member __.LastName = lastName
  member __.Age = age

// DemoClassのインスタンスを生成する
let value = DemoClass("Taro", "Hoge", 25)

// クラスのインスタンスをマッチングする
// "{ ... }"によるパターンの記法については後述
match value with
// エラー FS1129: 型 'DemoClass' にフィールド 'FirstName' が含まれません
| { FirstName=fn; LastName=ln; Age=a } ->
  System.Console.WriteLine(
    "Name={0} {1}, Age={2}",
    fn, ln, a)

# クラス型は上記のように定義しますが、詳細は省きます。コンストラクタが定義されること、プロパティが定義されてコンストラクタの引数が渡されることに注目してください。

しかし、マッチ式でプロパティが認識できません。これまで見てきたように、マッチ式は分解を行うこともできますが、単に値が一致することを確認する場合もあります。その両方の構文を満足させるためには、上記のような構文をサポートするだけでは不十分で、DemoClassのインスタンスを任意の値で初期化できて、かつ副作用が存在しない事が明らかであるような、宣言的な手段が必要です。

F#には「レコード型」という種類の型があり、この目的に使うことができます。レコード型の中身(IL)は単なるクラス型ですが、F#上では定義の構文が異なります。

// レコード型を定義する
type DemoRecord = {
    FirstName : string
    LastName : string
    Age : int
}

// レコード型の初期化(型は推測される)
let value = {
    FirstName = "Taro"
    LastName = "Hoge"
    Age = 25
}

// レコード型をマッチングする
match value with
| { FirstName=fn; LastName=ln; Age=a } ->
  // レコード型の各フィールドを分解できる
  System.Console.WriteLine(
    "Name={0} {1}, Age={2}",
    fn, ln, a)

レコード型を使うと、判別共用体と同じく型推論が働くようになります。上記の例ではvalueのインスタンスを生成するのに、型名”DemoRecord”を書く必要がありません。レコード型はF#によってフィールド(FirstName・LastName・Age)の初期化方法が管理可能な形で定義されるので、式だけで初期化が可能になり、パターンマッチングでもフィールド名を自動的に特定して値の分解もできるようになるのです。

レコード型のパターンには、”{ … }”という記法を使う事が出来ます。コード例に示した通り、「フィールド名=(マッチ式)」として記述できます。右辺を「マッチ式」と書いた通り、ここに更にネストしたマッチ式を書くことが出来ます。

さらなる例は省きますが、このように、レコード型もこれまでのマッチ式と柔軟に組み合わせて使用することができます。


真打ち・アクティブパターン

ここまでのパターンマッチングでも、もうかなりの応用力がある武器ですが、今まではささやかな序章だった・真のラスボスは云々、と言ったら信じてもらえますか? (*´Д`)

私が真面目にF#をモノにしようと決めたきっかけが、このアクティブパターンです。アクティブパターンは大きく2種類あるのですが、順番に解説します。どちらのアクティブパターンも「パターンマッチングを動的に実行できる」事が特徴です。

動的な分解

前節で、クラス型のメンバーを直接キャプチャ出来ないため、レコード型を使用するという例を示しました。アクティブパターンを使用すれば、クラスのメンバーを直接分解・キャプチャさせることが出来ます。

// DateTimeを分解してタプルで返すレコグナイザー関数
let (|Date|) (dt: System.DateTime) =
  dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.DayOfWeek

let now = System.DateTime.Now

// DateTimeをマッチングする
match now with
// レコグナイザーを使ってタプルに分解する
| Date (yyyy, MM, dd, HH, mm, _, _) ->
  System.Console.WriteLine(
    System.String.Format(
      "yyyy:{0} MM:{1} dd:{2} HH:{3} mm:{4}",
      yyyy, MM, dd, HH, mm))

# 本来DateTimeはクラスではなくて構造体ですが、違いはありません。

マッチングを動的に判定するには、「アクティブレコグナイザー関数」を定義します。この関数の名前は、何かしら魔術めいた括弧 “(| … |)” で括られていますが、これは「バナナクリップパターン」(バナナのように見えますね?)と呼びます。

中身は単なる関数です。したがってどのような値でも返すことが出来、返された値がマッチ式でキャプチャ出来るようになります。ここでは、分解した結果をタプルで返しているので、マッチ式でもタプルで受け取っています。そして、キャプチャしたくない要素はアンダースコアで捨てることもできます。

値を解析して分類分けできる

与えられた値に応じた結果を動的に変えられると便利です。例えば、判別共用体はTagによってマッチの有無が(静的に)判定されますが、この判定を動的に実行できるようなものです。

// ゼロ・奇数・偶数を識別するレコグナイザー関数
let (|Zero|Even|Odd|) value =
  match value, (value % 2) with
  | 0, _ -> Zero
  | _, 0 -> Even (value / 2) // 除算結果を返す
  | _, 1 -> Odd (value / 2)  // 除算結果を返す

let value = 131

match value with
// レコグナイザーを使って判定とキャプチャを行う
// ゼロにマッチ
| Zero ->
  System.Console.WriteLine("Zero")

// 偶数にマッチ
| Even half ->
  System.Console.WriteLine(
    System.String.Format(
      "Even: HalfValue={0}", half))

// 奇数にマッチ
| Odd half ->
  System.Console.WriteLine(
    System.String.Format(
      "Odd: HalfValue={0}", half))

// (Zero・Even・Odd以外の値にはなり得ない)

レコグナイザー関数の名前は、更に奇妙な事になっています。この例では、レコグナイザーの結果として取りうる値を “|” で区切ります。値がゼロ・偶数・奇数に応じて、”Zero”, “Even” 又は “Odd” の値を返すことを意味します。また、EvenやOddの場合は、除算の結果(value / 2)も返しています。

このレコグナイザーをマッチ式で使うと、マッチングの実行時に関数が呼び出され、値を動的に判定してZero・Even・Oddを返すので、それぞれのマッチ式にマッチします。Zeroの場合は値が得られませんが、EvenとOddは除算結果が返されるので、値をキャプチャすることが出来ます(half)。

また、このレコグナイザーを使うと言う事は、値がZero・Even・Oddの何れかにしか分類されないことがはっきりしているため、それ以外の値をとる事がありません(その他にマッチする式が不要)。

このマッチ式を見ていると、判別共用体を動的に判定しているかのように見えますね。最初のswitch-caseの説明で、数値や文字列だけではなく、任意の値(インスタンス)に対してEqualsやIEquatableを使ってカスタムコードで判定を行うことができれば良いのに、という話をしました。レコグナイザー関数を使えば、そういった判定を安全に行うことが出来ます。

値を解析してマッチ式の判定に影響させる

上記のレコグナイザーは、取りうる結果の種類があらかじめ完全に把握されている(Zero・Even・Oddの何れかしかない)場合に使えます。しかし、判断が該当しない場合は、単に「該当なし」として判定したい事もあります。

// 正の偶数を識別するレコグナイザー関数
let (|IsPositiveEven|_|) value =
  if value < 0 then
    None  // 該当しない
  else if (value % 2) = 1 then
    None  // 該当しない
  else
    Some (value / 2)  // 該当したので除算した結果を返す

let value3 = 131

match value3 with
// レコグナイザーを使って判定とキャプチャを行う
| IsPositiveEven half ->
  System.Console.WriteLine(
    System.String.Format(
      "Plus & even: HalfValue={0}", half))

// 正の偶数ではなかった
| v1 ->
  System.Console.WriteLine(
    System.String.Format(
      "Other: {0}", v1))

このレコグナイザー関数は、末尾にアンダースコアを付けることで「パーシャルレコグナイザー」として機能するようになります。パーシャルレコグナイザーは、戻り値が “Option型” の値で返される事を想定します。

Option型は判別共用体で、”Some”と”None”のTagを持ちます。C#で言うところの”Nullable”に近いのですが、存在するならその値、または値が存在しないと言う事を表し、判別共用体なので安全に使用できます。上記の例では、与えられたvalueが負数か0なら”None”、それ以外の場合はその値の1/2を返すようになっています。

このレコグナイザーを使用すると、マッチ式の評価は “Some” の場合にだけ変換された値がキャプチャされ、”None” の場合はそのマッチ式がマッチしなかったことになり、他のマッチ式を試行します。従って、この場合は変換に失敗したマッチを用意する必要があります(マッチが漏れていると警告が発生します)。

アクティブレコグナイザーを使うことで、これまで静的な宣言による判定しかできなかったコードで、動的に複雑な判定を行わせることが出来るようになります。しかもその実装は単なる関数であり、判定の詳細を関数内に完全にカプセル化することが出来ます。こうして設計されたレコグナイザーは、他のパターンマッチングのすべてのパターンと直交的に組み合わせて使用できるため、非常に高い表現力・安全性・応用性を持ちます。


まとめ

F#でのパターンマッチングを、これまでの一般的な比較・分岐処理と対比させて解説しました。私の感想としては、やはりアクティブパターン強し!と言う所です。私はC#のLINQも好きですが、LINQが凄かったのは、IEnumerable<T>による統一された集合処理と、拡張メソッドによる直交性のある独自の拡張が可能であったことだと思います。F#で直接対比するとすればシーケンス処理がそれに該当するのですが、正直すでにLINQが当たり前なので、F#で同じことが出来ることはある意味当然というイメージでした(むしろ、F#にはシーケンス処理を汎化した「コンピュテーション式」があり、そっちはこれまた大海原です)。

パターンマッチングの良いところは、今までであれば「デザインパターン」に落とし込んで回避しなければならなかったような複雑な構造を、シンプルに、簡単に、わかりやすく、安全に記述出来て、検証可能で、しかもすぐに応用することが出来るようになった事だと思います。例えば、前半で示したパターンの組み合わせについて、抽象クラスを導入して実装を派生させて…という考え方ももちろん可能です。しかし、F#の世界ではそんな事をしなくても、タプルや判別共用体とパターンマッチングを組み合わせるだけで解決できてしまいます。

プログラミング言語の進化としては、ジェネリックプログラミング以来のインパクトを感じました。F#ではこれらがもう当たり前の世界になっていて、C#もこれからパターンマッチングを取り込んでいくと思います。応用性の高い、直交性のある拡張を期待したいです。

補足: パターンマッチングのマッチ式内で使用できるパターンは、すべて網羅していません。より複雑なパターンもサポートされています。この記事で解説した内容を把握していれば、それらについても容易に理解できると思います。詳しくはMSDNを参照してください。


修正履歴

  • タプルの型Tuple<…>が隠され、Item1, Item2等のプロパティに直接アクセスできないので修正しました。もちろん、リフレクションを使用した場合はこの限りではありません。
  • F#のリストは順方向リンクリストであることを明示しました。内部的にはFSharpList<‘T>というクラスです。
  • “:?”や”[|…|]”や”(|…|)”は演算子ではないので、記述を改めました。MSDNには「パターン」と書かれています。パターンという語は一般的過ぎて説明には適さないと思っているのですが、合わせるようにしています(まだ文書全体では統一されていません)。
  • “:?”パターンでマッチさせたい目的が、型の動的判断ぐらいしかないという記述は無くしました。実際にはすべてのパターンは直交的に組み合わせ可能なので、実現したい判断次第ですね。
  • 関数名をよりふさわしく修正し、int.TryParseという使われていない解説を修正しました。
  • OptionとNullableの関係について修正しました。Optionは判別共用体である事にフォーカスさせました。

※まだ修正作業中です。気になる方はfsugjpでの指摘を参照してください。

continuatioN Linking – NL名古屋

「continuatioN Linking」という題目で、NL名古屋で登壇してきました。

NとLの字さえ使っていれば何でも良いというカオスなショートセッション会なのですが、内容は非常に濃くて面白いものばかりでした。とても満足度が高かったです。Togetterのまとめはこちら

今回写真撮り忘れてしまったのでありませんが、50名超えと大入りな感じでした。


継続ネタ

継続渡しスタイル(Continuation Passing Style)の話に絡めて、.NET TaskとF# Asyncのシームレスな相互運用を行うネタを発表してきました。

何しろここは名古屋 (;´Д`) なので、このネタで行くにはまだ「ひよっこ」で恐怖しかない感じでしたが、掴みもスベる事なく発表できたのでうれしかったです。一部の方には触れるものがあったようで、「おおーー!!」という声はやって良かった感ありました。

発表はショートセッションですが参加者が多いこともあり、最終的に時間制約が10分となって苦しいところでしたが、予定通り進行を無視して「継続」させていただきましたありがとうございます。


補足

さて、内容は駆け足だったので少し補足しておきます。

FSharp.Control.FusionTasks.128GitHub: kekyo/FSharp.Control.FusionTasks
NuGet (F# 2.0): FSharp.Control.FusionTasks.FS20
NuGet (F# 3.0): FSharp.Control.FusionTasks.FS30
NuGet (F# 3.1): FSharp.Control.FusionTasks.FS31
NuGet (F# 4.0): FSharp.Control.FusionTasks.FS40

検討

アイデアはすぐに思いついたものの、実現可能かどうかが良くわかりませんでした。そもそも素人の浅はかで、.NET TaskクラスとF# Asyncクラスは似ているな、もしかしたら簡単に相互運用できるかも?と思ったのがきっかけです。

  • わざわざ似ているものが併存している事に何か特別な理由があるのかと思っていましたが、実はF# AsyncのほうがTaskよりも歴史が古く、CLR 4.0の形も無いころから既に非同期ワークフローがサポートされていました(.NET 2.0から実現している)。だからこの疑問は不適切で、.NET TaskはF#に依存しないように改めて設計しなおしたのだと思われます。
  • F#非同期ワークフローは、AsyncBuilderクラスを使用する、一種の構文糖(computation式)だと理解しています。F#の場合、任意の型に(インスタンスであろうとスタティックであろうと)拡張メンバー(C#で言う所の拡張メソッド)を生やす事ができるため、AsyncBuilderに.NET Taskを扱うメンバーを増やすことで、非同期ワークフローでシームレスに.NET Taskを扱えるのではないかと考えました。
  • 一番大きな動機は、HttpClientなどのネイティブ非同期対応ライブラリを、そのままF# Asyncで使うことが面倒であったということがあります。F#にも標準で.NET Taskのためのサポートはあります。「Async.AwaitTask関数」がそれですが、この関数は見ての通りT型が特定されなければならず、非ジェネリックTaskをAsync<unit>に変換できません。また、逆の変換(AsyncからTask)への変換方法もありません。逆の変換ができると、F#で書いた非同期ワークフローのインスタンスを、C#側でawait出来るようになるため、さらに応用性が広がります。

※ computation式については、「詳説コンピュテーション式」が詳しいです。

非同期コンテキストの結合

.NET非同期処理(async-await)と例外の制御 で持ち出した「タスクコンテキスト」という用語なのですが、もはや統合されたこの世界では「非同期コンテキスト」としか言いようがないですね。

「非同期コンテキスト」をシームレスに結合した場合、維持されなければならない重要なポイントがあります。それは、「同期コンテキスト(SynchronizationContext)」「キャンセルトークン(CancellationToken)」です。うーん、コンテキスト紛らわしい (*´Д`)

.NET Task側の事はある程度わかっているのですが、F#非同期ワークフローではこれがどのように維持されるのかがわからず、シームレスな相互運用が実現するにはココの担保が不可欠だと考えていました。

fsugjpの方々とやりとりしたり(あまりまとまりは無いです)、MSDNを調べたりして、以下の事がわかりました (Thx fsugjp!!)

  • F#にはAsync.FromContinuations<T>関数があり、これを使うとコールバック関数を経由してAsyncクラスの結果を制御できます。これは丁度TaskCompletionSource<T>クラスを使ってTaskを間接的に操作することに相当するので、Taskの結果を反映させることに使えます。
    余談ですが、初見ではあの引き数で何ができるのか、さっぱりわかりませんでした(つまりビギナー認定w)
  • 「Continuations」という単語の響きに導かれ、逆のパターンにはAsync.StartWithContinuations<T>関数を使えばよいことがわかりました。こっちはもっと単純で、正常・異常・キャンセルの継続処理をCPS形式で渡すだけです。

同期コンテキスト自体は、Asyncクラス内で非同期待期に使用しているので、F#非同期ワークフローで使う限り特に問題なさそうです。Task.ConfigureAwaitメソッドに相当する同期コンテキストのキャプチャ制御についてはむしろF#の方が柔軟性があり、Async.SwitchToContextAsync.SwitchToThreadPoolAsync.SwitchToNewThread関数を使用して、いかようにでも同期コンテキストを操ることができます。

そしてキャンセルトークンの方ですが、F#側はAsyncクラスのインスタンスを何らかの方法で実行するときにトークンを保持し、それがAsyncクラスの非同期コンテキストの情報として保持されて使われます。簡単に言うと:

// 非同期ワークフロー内で非同期スリープ(Task.Delayと概念は同じ)
let asyncBody = async {
  // (トークンはこの非同期ワークフローのコンテキストに暗黙に伝搬している)
  do! Async.Sleep(10000)
}

// キャンセルトークンを準備
let cts = new CancellationTokenSource()

// 非同期ワークフローをキャンセルトークンありで同期実行する
Async.RunSynchronously(asyncBody, Timeout.Infinite, cts.Token)

※ 注意: RunSynchronouslyは説明のために使用しています。本当は使わないように書くべきです。Task.Waitに相当します。

上記のような状態でSleepしているときにトークンがシグナル状態となると、正しくSleepが中断されます。Task.Delayの場合はキャンセルトークンを受け取るオーバーロードを使用していないと中断できませんが、F#非同期ワークフローの場合は、現在の非同期コンテキストに伝搬するトークンが管理され、Async.Sleepはそれを監視しているので正しく中断されるのです。

伝搬の手法は魔術的な何かでもなんでもなく、RunSynchronouslyで渡されたトークンがAsync.CancellationToken プロパティで管理されていて、Async.Sleepはこれを参照しているからです。実際、Task.Delayと異なり、Async.Sleepにはトークンを明示的に指定するオーバーロードはありません。

作ってみた感想

この部分、私的にはとても良くできていると思いました。というのも、F#非同期ワークフローの「非同期処理」に関する範囲と制約は、すべてワークフロー(computation式)内に閉じているからです。キャンセルトークンの管理もここで閉じているので、操作する側はAsyncクラスのインスタンス=非同期コンテキストと言うように、はっきりとイメージ出来ます。

対照的に.NET TaskとC# async-awaitではこれが曖昧で、ビギナーにasync-awaitの事を説明するためには、どこからが非同期コンテキストなのかをしつこいほどに解説する必要がありました。また、デフォルトではキャンセルに対する操作は完全に使用者任せとなり、Task.Delayのようにトークンを引き渡すシグネチャを明示的に設計に盛り込む必要があります。私はこのことに気が付くのが遅かったために、フレームワークインターフェイスの変更という大手術を行う羽目になったことがあります。そして、たとえ周到に準備しても、使用者がトークンを渡すのを忘れると台無しとなり、トークンをどこで管理するのかということも(使用者が)考える必要があります。

物事にはトレードオフがあるはずで、.NET Taskとasync-awaitが後発でありながらこういう選択をした理由も恐らくあるのでしょう。想像出来ることと言えば、async-await方式は、メソッド内のコードに「await」という単語が入るだけで、それ以上の大幅な変化がないと言うことです。しかし、実用的な非同期コードをasync-awaitで書いたことがある方ならわかると思いますが、現実には非同期であることを意識したコードを書かないと、pitfallに落とされます(一例を例外の記事でも書きました)。

F#非同期ワークフローでは、「async」に囲まれたcomputation式内でしか使えず、computation式では「let!」「do!」などの普通には使わない予約語を使う必要があります。ただ、上で述べたような問題とのトレードオフとしては悪くない選択だなと思います。説明も簡単なのが大きいですね。

そして、一番最初の「もしかしたら簡単に相互運用できるかも?」というのは、「それなりにイケた」という感じですが、やってみた後の感想としては、.NET TaskとF# Asyncクラスはそれぞれ役割もインターフェイスも似ているにも関わらず、現実には設計思想からして全く違うものだという印象が強くなりました。

後は、現在進行中のもう一つのプロジェクトと合わせて、何とかF#のコードが書けるようになったと言うことかな。人に説明するにはまだいろいろ足りてない感じですが、F#や関数型言語の面白さの一端は見えてきました。

それではまた。