読者です 読者をやめる 読者になる 読者になる

AutoOpenAttributeについて

F#

F# Advent Calendar 2014 - connpassの2日目です。

今回は、AutoOpenを紹介します。

AutoOpenとは、モジュールを自動でopenする仕組みです。主に言語やフレームワークでよく使うモジュールを提供するときに使います。主にライブラリ開発者に対する機能で、利用者は使うことは無いと思います。 実際に使うときは、AutoOpenAttributeを使用します。

しかし、あまりにも開くモジュールが多すぎると利用者はビックリしてしまうので、ほどほどにしましょう。

MSDNを見ても何の事か分からないので、F#の言語仕様書を見てみましょう。「Special Attributes and Types」という章にAutoOpenAttributeの記述があります。

When applied to an assembly and given a string argument, causes the namespace or module to be opened automatically when the assembly is referenced. When applied to a module without a string argument, causes the module to be opened automatically when the enclosing namespace or module is opened.

アセンブリと、モジュールに対してAutoOpenAttributeを適用できる」と書いてあります。それぞれの使い方を説明します。

モジュールに適用する場合

よく使うパターンです。モジュールに対してAutoOpenをつけると、親のモジュール、名前空間がopenされた時に、自動でopenされます。

使い方はこんな感じ。

module A =
  let f x = x
  [<AutoOpen>]
  module B =
    let g x = x * 2 

open A
let x = g 1 (* Bモジュールの関数を使える *)

アセンブリに適用する場合

アセンブリにAutoOpenを付け、開きたいモジュール名を指定します。AutoOpenが付与されたアセンブリをプロジェクトの参照に追加しただけで、自動でopenされるようになります。

使い方はこんな感じ。例ではモジュール定義のすぐ下に書いてますが、実際に使うときはAssemblyInfo.fsなどに書くといいと思います。

module A =
  let f x = x
  module B =
    let g x = x * 2 
[<assembly: AutoOpen("A.B")>]
do ()

こちらの機能は、強力すぎるので使う機会が少ないと思いますし、使わないほうがいいと思います。

F# では、FSharp.Coreがこの方法を使っています。いつも使えるListモジュール等は、アセンブリに対するAutoOpenで提供されています。 ソースコードこの辺りです。 特別扱いをするのではなく、言語の機能を使って提供しています。素晴らしいですね!

つまり、AutoOpenを外したFSharp.Coreを作れば、明示的にopenしないとListモジュールなどは使えないってことでしょうか? 既存のソースコードコンパイルエラーになるはずです(試してないけど)。仕事場で隣の人が席を外している間に悪戯してやりましょう。

F# のネタリスト

F#

知りたいこといっぱいあるので詳しい教えて下さい!

  • F#に便利なVSアドインまとめとか読みたいです
  • 判別共用体の名前付きフィールドについて知りたいです
  • F#3.1のprintfのパフォーマンスについて知りたいです
  • C#で定義されたクラスをF#から扱う時のtipsについて知りたいです
  • F#3.1の新機能詳細(まとめじゃない)について知りたいです
  • 判別共用体の値がタプルかそうじゃないかでの挙動の違いについて知りたいです
  • useはコンピュテーション式の中でも使えるんだよってことを知りたいです
  • リフレクションでOptionを扱う時の注意点を知りたいです
  • アクティブパターンのアクティブっぷりが知りたいです
  • CompilationRepresentationとかAutoOpenとかRequireQualifiedAccessとかF#コンパイラに関わる属性について知りたいです
  • CustomEqualityAttribute, CustomComparisonAttributeとか知りたいです
  • abstractとdefaultについて知りたいです
  • リフレクションを多用しててobjを使わざるを得ないけど、なるべくobjを減らす方法を知りたいです
  • FAKEというF#製ビルドツールについて知りたいです
  • F#最速(予定)のFsAttoparsecというパーサコンビネータについて知りたいです
  • F#コンパイラよりVSインテリセンスの方が賢い場合があるってことについて知りたいです
  • TypeProviderについて知りたいです
  • クラス・インターフェースを使った時の、キャストが必要なとき、必要ないときを知りたいです
  • doを使えばスコープが作れるってことを知りたいです
  • レコードとかクラスを作ったら、それを扱うモジュールと関数を作っておくと型推論が捗るよって知りたいです
  • 型に別名つけたら一緒にシグネチャファイルも用意するとライブラリ利用者が喜ぶよって知りたいです
  • よく使うけど標準ライブラリに定義されてないから、作っておくと良い関数知りたいです
  • 入力補完のためならクラス使用を辞さない覚悟について知りたいです
  • クラスのコンストラクタ呼び出し時にプロパティも同時に初期化する方法知りたいです→クラスのコンストラクタ呼び出し時にプロパティも同時に初期化する - ぐるぐる~
  • Genericなクラス、関数でも<_>って書いとけば型推論が頑張ってくれること知りたいです
  • openした結果モジュールが被ってしまった場合でも両方のモジュールの関数を扱えること知りたいです → id:rika0618 が教えてくれるそうです!
  • Printfモジュールの使い方知りたいです
  • F#のstaticイニシャライザが呼ばれたり呼ばれなかったりについて知りたいです
  • C#ライブラリからnullが渡ってきた時の対処法知りたいです
  • F#最強の今秋リリースするらしい柿っていうテスティングフレームワークについて知りたいです
  • 型拡張について知りたいです
    • 8.12 Type Extensions
  • 静的に解決された型パラメータについて知りたいです
  • 文字列を比較するとき注意しないといけないこと知りたいです

F# のデバッグ方法 #fsharp

F#

F#のデバッグには、Visual Studioによる制限と、F#特有の、二種類の辛さが(C#に比べて)あります。本記事では、Visual Studio上での制限や、F#特有のデバッグ方法を紹介します。

また、Visual Studioしか知らないので、他の環境のデバッグについては、得意な人がきっと教えてくれます?

Visual Studio上での制限

Visual StudioでのF#のデバッグMSDNF# のデバッグ)に記事があります。特に注目したいのが、3つ目の制限の、

デバッガーは F# 式を認識しません。 F# のデバッグ中にデバッガー ウィンドウまたはダイアログ ボックスに式を入力するには、式を C# の構文に変換する必要があります。 F# の式を C# に変換するときは、C# では等価を示す比較演算子として == を使用しますが、F# では単一の = を使用することに注意してください。

です。ウォッチ式や、イミディエイトウィンドウでも同様に、C#の式が必要になります。F#の構文をC#へ変換する方法を紹介します。

モジュールに定義された関数、ラムダ式の呼び出し

関数呼び出しはメソッド呼び出しと同様の方法で呼び出します。let f x y z =...という関数は、F#だとf 1 2 3で呼び出せますが、C#へはf(1, 2, 3)と変換します。カリー化されている場合でも、3引数のメソッドとして扱えます。実は、F#の関数は、C#からはメソッドとして見えるんです。

ローカル関数、ラムダ式の呼び出し

ローカル関数は関数値Microsoft.FSharp.Core.OptimizedClosures.FSharpFuncとして扱われます。f 1 2 3と呼び出したい場合は、f.Invoke(1).Invoke(2).Invoke(3)と変換します。

また、モジュールに定義された関数でも、関数値の場合は、Invokeメソッドで呼び出さなければなりません。 見分ける方法は、関数の場合はシグネチャ'a -> 'b -> 'cとなっており、関数値の場合は、('a -> 'b -> 'b)と括弧で囲われます。

演算子の呼び出し

演算子は、たとえば、let (++) x y = ...という演算子は、3++4とは書けませんop_PlusPlus(3, 4)と変換します。

MSDN演算子のオーバーロード (F#))の定義にしたがって、演算子メソッド名に変換します。そして、メソッドとして呼び出します。

変換例を幾つか挙げます。

演算子 メソッド
+ op_Addition
++ op_PlusPlus
+- op_PlusMinus

また、型に定義された演算子は、staticメソッドとして呼び出します。

ブレークポイント

基本的にどこでも置けます。ラムダ式の中や、コンピュテーション式の中でも大丈夫です。 ただし、ラムダ式の中から外側の変数の値は見えないようです。コンピュテーション式もメソッド呼び出しとラムダ式に展開されるので、同様です。

シャドーイングした場合は、全てシャドーイング後の値になります。

コンピュテーション式の中でステップインすると、C#のクエリ式のように、コンピュテーション式の実装の方に迷い込んでしまうので、注意しましょう。

パイプライン演算子の間にブレークポイント

F#ではパイプライン演算子|>をよく使います。ただし、簡単には途中の値を覗けません。覗く方法を幾つか紹介します。

式を変形する

簡単ですが面倒くさい方法です。計算過程を変数に束縛します。

f x |> g

上記式を次のように変換します。デバッグが終わったら元に戻します。

let a = f x
g a

この方法は確かに値を覗けるのですが、修正が大きすぎてミスりそうです。ですが、もう少し修正が小さい方法があります。

ID関数

let bp x = xのような関数を定義し、デバッグ時に見たいところに差し込みます。ブレークポイントbp関数の中に置いておきます。 先ほどと同じように、デバッグが終わったら削除します。

let bp x = x (* ここにブレークポイント *)
f x |> bp |> g

もしくは、直接ラムダ式を書き、ラムダ式の中にブレークポイントを置きます。

f x |> (fun x -> x) |> g

パイプライン演算子を上書きする

今までに挙げた方法では、コードを書き換える必要がありました。The trace-mode pipeline operator | F# Snippetsで見つけたのですが、パイプライン演算子を上書きし、演算子の中にブレークポイントを置く方法があります。

let (|>) x f = let result = f x in x (* ここにブレークポイント *)
f x |> g

そして、演算子を#ifディレクティブで囲っておけば、リリース時には元のパイプライン演算子に置き換わります。

ただし、この方法では、スコープ内の全てのパイプライン演算子を上書きしてしまうので、止めたい位置で止めるのが難しくなります。

どの方法も一長一短なので、好きな方法を使えばいいと思います。

Visual F# Power Tools の紹介

F#

F# Meetup in Tokyo Part 1 - connpassの発表ではスライドを使わなかったので、紹介したサイトのURLをメモしようと思います。

Visual F# Power Tools

Visual F# Power Toolsとは、Visual Studioの貧弱なF# エディタに機能を追加してくれるツールです。Visual Studio 2012、2013のProfessional以上で利用できます。Expressでは使えません!!!

ユーザーガイドに機能一覧があります。サイト自体は英語ですが、各機能のページに動画gifがあるので、雰囲気でわかると思います。

ダウンロードとインストールは、VisualStudioGallery http://visualstudiogallery.msdn.microsoft.com/136b942e-9f2c-4c0b-8bac-86d774189cffから、
または、Visual Studio拡張機能から行います。

不具合の報告はgithub https://github.com/fsprojects/VisualFSharpPowerToolsに、
要望はuser voice http://vfpt.uservoice.com/
報告しましょう。 user voiceは気になる機能をvoteするだけでもOKです。職場ではたまに組織voteが行われています。

また、F#言語自体のuser voice https://fslang.uservoice.com/もあります。

F# Compiler Services

Visual F# Power ToolsはF# Compiler Servicesを使って実現されています。C#でいうRoslynです。

http://fsharp.github.io/FSharp.Compiler.Service/ja/

ライブラリ開発者のためのObsoleteAttribute

F#

F# Advent Calendar 2013の10日目です。

ライブラリを開発していると、どうしても古くなり使って欲しくないAPIが出てくると思います。古いAPIは思い切って削除しても良いのですが、互換性を考えてしばらくの間、残しておくのがライブラリ利用者にとって親切でしょう。

ただ、残しておいても利用者は気づきにくいので、.NET FrameworkではObsoleteAttributeを使って、コンパイラの警告として利用者に知らせることができます。

簡単な使い方は、MSDNを参照してください。

http://msdn.microsoft.com/ja-jp/library/system.obsoleteattribute.aspx

しかしこのObsoleteAttribute、F# では期待通りに動かないパターンがいくつかあります。 動くパターンと動かないパターンをそれぞれコードで説明しようと思います。期待と反する動作をする場合にはコード中のコメントに"!"を付けました。

レコード

レコード定義にObsoleteAttributeをつけた例です。 レコードの場合、型推論された時、Obsoleteなレコード型の警告が出ません。ただし、let f x = x.Aのパターンで推論された場合は警告が出ます。中途半端ですね。

[<Obsolete>]
type OldRecord = {
  A: int
}

(* レコード生成 *)
let old = { A = 1 }             (* 警告なし! *)
let old2: OldRecord = { A = 1 } (* 警告 *)
let old3 = { OldRecord.A = 1 }  (* 警告なし! *)

(* 関数定義 *)
let f x = x.A                   (* 警告 *)
let g { A = a } = a             (* 警告なし! *)
let h (x: OldRecord) = x        (* 警告 *)

次は、レコードのメンバに対してObsoleteAttributeを付けた例です。Obsoleteなメンバの場合は期待通り動作するようです。

type OldRecord = {
  [<Obsolete>]
  OldMember: int
  A: int
}

(* レコード生成 *)
let old = { OldMember = 1; A = 1 }                       (* 警告 *)
let old2: OldRecord = { OldMember = 1; A = 1 }           (* 警告 *)
let old3 = { OldRecord.OldMember = 1; OldRecord.A = 1 }  (* 警告 *)

(* 関数定義 *)
let f x = x.A               (* 警告なし *)
let g x = x.OldMember       (* 警告 *)
let h { A = a } = a         (* 警告なし *)
let i { OldMember = o } = o (* 警告 *)
let j (x: OldRecord) = x    (* 警告なし *)

判別共用体

判別共用体については、どのパターンも期待通り動作します。

判別共用体の定義にObsoleteAttributeを付けた例です。

[<Obsolete>]
type OldUnion =
  | A
  | B of int

let a = A    (* 警告 *)
let b = B(1) (* 警告 *)

let g (x: OldUnion) = x                 (* 警告 *)
let g = function  A -> "a" | B _ -> "b" (* 警告 *)

次は、判別共用体のケースに対してObsoleteAttributeを付けた例です。

type OldUnion =
  | [<Obsolete>] A
  | B of int

let a = A    (* 警告 *)
let b = B(1) (* 警告なし *)

let g (x: OldUnion) = x                (* 警告なし *)
let h = function A -> "a" | B _ -> "b" (* 警告 *)
let i = function B _ -> "b" | _ -> "a" (* 警告なし *)

クラス

クラス定義に対して、ObsoleteAttributeを付けた例です。 変数の型を推論した場合に警告が出ません。また、F# では型とコンストラクタ呼び出しに同じ名前を使いますが、別物のようです。

[<Obsolete>]
type OldClass(a: int) =
  member val A = a with get, set
  
let a = OldClass(1)           (* 警告なし! *)
let b: OldClass = OldClass(1) (* 警告 *)

let f (x: OldClass) = x.A     (* 警告 *)

実はこの、変数の型を推論した場合に警告が出ない問題は、C#の、変数のvar宣言にも存在します。

次は、メンバに対して、ObsoleteAttributeを付けた例です。期待通りに動作します。

type OldClass(a: int, b: int) =
  [<Obsolete>]
  member val OldMember = a with get, set
  member val B = b with get, set

let a = OldClass(1, 1).OldMember (* 警告 *)
let b = OldClass(1, 1).B         (* 警告 *)

次は、コンストラクタに対して、OsboleteAttributeを付けた例です。期待通りに動作します。

type OldClass [<Obsolete>](a: int, b: int) =
  [<Obsolete>]
  new () = OldClass(1, 1)  (* 警告 *)
  new (a) = OldClass(a, 2) (* 警告 *)

  member val A = a with get, set
  member val B = b with get, set

let a = OldClass(1, 2) (* 警告 *)
let b = OldClass()     (* 警告 *)
let c = OldClass(2)    (* 警告なし *)

モジュール

モジュールの定義に対して、ObsoleteAttributeを付けた例です。期待通りに動作します。

[<Obsolete>]
module OldModule =
  let x = 1

let a = OldModule.x (* 警告 *)
open OldModule      (* 警告 *)
let b = a           (* 警告なし *)

次は、モジュール内の変数に対してObsoleteAttributeを付けた例です。期待通りに動作します。

module OldModule =
  [<Obsolete>]
  let oldVal = 1

let a = OldModule.oldVal (* 警告 *)
open OldModule           (* 警告なし *)
let b = oldVal           (* 警告 *)

次は、AutoOpenAttributeのついたモジュールに対して、ObsoleteAttributeを付けた例です。 親モジュールをopenし、間接的にopenされた場合は警告が出ません。しかし、明示的にopenした場合は警告が出ます。

module Module =
  [<Obsolete>]
  [<AutoOpen>]
  module OldModule2 =
    let x = 2

open Module            (* 警告なし! *)
let a = x              (* 警告なし *)
open Module.OldModule  (* 警告 *)

まとめ

さまざまな例を挙げ、ObsoleteAttributeの動作を見てきました。 結論としては、「型推論、AutoOpenといった暗黙的に使用される場合は警告が出ない」と言えそうです。 せっかくObsoleteAttributeを付けたのにライブラリ利用者に気づいてもらえなくて悲しいです。

この問題に対して、次の対策がとれそうです。

  • レコードをObsoleteにする場合は全てのメンバーにもObsoleteAttributeを付ける
  • クラスをObsoleteにする場合はコンストラクタにもObsoleteAttributeを付ける
  • AutoOpenされるモジュールをObsoleteにする場合はモジュール内の要素全てにObsoleteAttributeを付ける

どれも面倒くさいですね!ていうかバグなんじゃねーのこれ?

恐ろしい standalone オプション

F#

この記事は、F# Advent Calendar 2012 14日目の記事です。

3ヶ月前の話しをします。僕はF#を使ってプロダクトに対するテストツールを作っていました。Webアプリケーションに対してリクエストを投げ、レスポンスやDBをチェックし、期待通りの動作をしているかチェックするツールです。

本番環境へデプロイして、テストツールを実行して動作を確認できれば、リリース完了となります。ですが、作業目前でとんでもない事実を知るのでありました。

テストツールを実行するマシンには! F#が!インストールされていない!インストールできない!!!
開発環境でしか動作しないツールを作るとかやる気あるんですかああああ

FShap.Core.dllを持っていけばいいじゃない

F#は実はただのライブラリなので、というかDSLなので、FShap.Core.dllを実行ファイルと同じディレクトリにコピーしておけば、F#で作ったプログラムを実行できます。
ですが注意が必要で、F# 2.0 のアセンブリには.NET Framework 2.0 用と .NET Framework 4.0 用の2種類があります。片方だけ利用している場合問題ないのですが、混在している場合は、コンパイルはできるのに実行できません。どちらかに統一してください。

F# コンパイラによるサポートもあります

FShap.Core.dllをコピーすればいいという話を先にしましたが、standaloneというコンパイラオプションを使えば、依存するアセンブリを成果物に含めることができます。このオプションをコンパイル時にしていすれば、F#がインストールされていない環境でもプログラムを実行できてしまいます。ナイスなオプションですね。
さらに、先ほど挙げた、.NET Framework 2.0用、4.0用のアセンブリが混在している場合はコンパイルが失敗します。コンパイルが成功したものは実行できる!

VS2012 での standalone オプションの指定方法

プロジェクトのプロパティを開き、ビルド > その他のフラグ に、--standalone と指定します。

standalone オプション利用時の注意点

standaloneオプションは出力形式がDLL、EXEのどちらでも利用できるのですが、EXEを出力するプロジェクトに指定するのが無難です。FShap.Core.dll等を取り込むため、standaloneオプションを指定して作成されたDLLと、FShap.Coreの両方を参照した場合、競合が起こりコンパイルができません。参照設定からFShap.Coreを除けば解決できるのですが、複数のプロジェクトを扱っていると面倒ですよね。

まとめ

この記事は僕が実際にハマった問題を元にしています。
standaloneオプションにより F# の無い環境でも実行できるようになりました。 妥協してF#を使わない理由がひとつ減ったと思います。

FParsecで遊ぶ

F#

この記事は、F# Advent Calendar 2011 6日目の記事です。

構文解析に使うライブラリはたくさんありますが、今回はFParsecを取り上げます。
FParsec は Haskell の Parsec というパーサコンビネータの F# 実装です。

僕の考える文法

F#を使っていて、「F#の文法じゃ僕の思うDSLを表現できない」って思う事が良くありませんか?
内部DSLを頑張るのも結構ですが、どうしても格好悪くなってしまいますよね。
そういう時は、迷わず外部DSLを作ってしまいましょう。

そして、DSLの文法を考えているとき、どうしても実装したい機能が一つありますよね。F#にもあるオフサイドルールです。
今時、閉じタグ、閉じ括弧なんてかっこ悪い!
でも、オフサイドルールって実装するのが面倒そうじゃないですか? どうやって作ればいいのでしょうか?

例題

こういう入力を、

parent
  child1
  child2
    grandchild1
  child3

こういう構文木に変換してください。

type Ast =
  | Node of string * Ast list
  | Leef of string

printfn "%A"した結果はこうです。

Some
  (Node
     ("parent",
      [Leef "child1"; Node ("child2",[Leef "grandchild1"]); Leef "child3"]))

戦略を考えよう

最初にどういう手順でパースしていくか考えます。括弧を擬似的に挿入する、という方法はどうでしょうか?
インデントが深くなったら括弧を開き、浅くなったら括弧を閉じるイメージです。

例題の入力を、以下のものとして扱います。

parent{
  child1
  child2{
    grandchild1
  }
  child3
}

単純に思いつく方法は、2回パースする方法ですね。UserState は使いません。
1回目のパースで括弧を挿入し、2回目のパースで実際に変換する方法です。これなら簡単にできそうですね。
ですが、それでいいのでしょうか? 1回のパースだけで行いたくないでしょうか?

パースは1回がいい!

では、どうすれば1回のパースで完了できるでしょうか。
括弧を挿入するルールは、インデントが深くなったら括弧を開き、浅くなったら括弧を閉じる、でした。
でもこれって、前行の深さ、新しい行の深さ、親要素の深さを覚えておく必要がありそうです。
つまり、パーサが状態を持つ、ということです。

オフサイドルールパーサの状態を Record で表すとこんな感じです。

type Context = {
  Levels: int list // 親要素の深さのスタック
  CurrentLevel: int // 前行の深さ
  NewLevel: int // 新しい行の深さ
}

そして、パーサで状態を扱うには UserState を利用します。
UserState を扱うには、次のパーサを利用します(FParsecでは状態の操作もパーサになっています)。

  • 取得 getUserState
  • 更新 setUserState, updateUserState

この Context を次のタイミングで更新します。

  1. 行頭のスペースをパースしたとき NewLevel を更新する
  2. インデントが深くなった時 Levels に CurrentLevel を push する。 CurrentLevel を NewLevel に更新する。
  3. インデントが浅くなった時 Levels から1つ pull し、CurrentLevel に設定する。

UserState と更新のルールさえ決まれば、オフサイドルールなんて勝ったも同然ですよね。

答え合わせ

ところが、素直に実装していったらとても長くなってしまいました。

module OffsideParser
open FParsec

type Ast =
  | Node of string * Ast list
  | Leef of string

type Context = {
  Levels: int list // これまでのインデントのスタック
  CurrentLevel: int // 解析中のインデント
  NewLevel: int // 新しく検出したインデント
}

let updateNewLevel newLevel c = { c with NewLevel = newLevel }
let updateCurrentLevel currentLevel c = { c with CurrentLevel = currentLevel }
let updateLevels levels c = { c with Levels = levels }

type Parser<'a> = Parser<'a, Context>

// 要素名のパーサ
let pName : Parser<_> = manyChars (letter <|> digit)

// インデントの深さを返すパーサ
let pSpace : Parser<_> = manyChars (pchar ' ')
let pIndent = pSpace |>> String.length

// 同じ深さの場合成功するパーサ
let pSameLevel state = parse {
  do! (fun stream -> if state.CurrentLevel = state.NewLevel then
                       Reply(ReplyStatus.Ok)
                     else
                       Reply(ReplyStatus.Error, messageError "same level error"))
      |>> ignore
  return ()
}

// 改行と共に次の行のインデントの深さを調べるパーサ
// EOFだったら次の行はインデントの深さが0とする
let pEndOfLine = 
  attempt (parse {
    do! eof
    do! updateUserState (updateNewLevel 0)
  }) <|>  parse {
    do! newline |>> ignore
    let! indent = pIndent
    let! state = getUserState
    do! updateUserState (updateNewLevel indent)
  }

// インデントが深くなっていたら成功するパーサ
let pOpenParen level = parse {
  let! state = getUserState
  do! (fun stream -> if level < state.NewLevel then
                        Reply(ReplyStatus.Ok)
                      else
                        Reply(ReplyStatus.Error, messageError "open paren error"))
      |>> ignore
  do! updateUserState (updateLevels (level :: state.Levels) >> updateCurrentLevel (state.NewLevel))
  return ()
}

// インデントが浅くなっていたら成功するパーサ
let pCloseParen level = parse {
  let! state = getUserState
  do! (fun stream -> if state.NewLevel <= level then
                        Reply(ReplyStatus.Ok)
                      else
                        Reply(ReplyStatus.Error, messageError "close paren error"))
      |>> ignore
  do! updateUserState (updateLevels (state.Levels.Tail) >> updateCurrentLevel (state.Levels.Head))
  return ()   
}

let pElement, pElementR = createParserForwardedToRef()

// 子要素のパーサ
let pChildren = parse {
  let! state = getUserState
  let currentLevel = state.CurrentLevel
  do! pOpenParen currentLevel
  let! lines = many pElement
  do! pCloseParen currentLevel

  return lines
}

// トップレベルのパーサ
do pElementR := parse {
  let! state = getUserState
  do! pSameLevel state
  let! name = pName
  do! pEndOfLine
  let! children = opt pChildren
  return match children with
         | Some c -> Node(name, c)
         | None -> Leef(name)
}

実行する方法です。

input = "parent
  child1
  child2
    grandchild1
  child3"
let context = { Levels = []; CurrentLevel = 0; NewLevel = 0 }
  match runParserOnString pElement context "" input with
    | Success(result, _, _) ->
      Some result
    | Failure(message, _, _) ->
      None

実行結果です。問題と一致しました。

Some
  (Node
     ("parent",
      [Leef "child1"; Node ("child2",[Leef "grandchild1"]); Leef "child3"]))

まとめ

僕がオフサイドルールな DSL を作った時の記録を元に、この記事を書きました。
構文解析楽しいです。実現したい文法があれば、妥協せずにどんどんパーサを書きましょう。