F#3.0で加速する言語指向プログラミング(LOP)。コンピューテーション式はもはやモナドだけのための構文ではない!!!
マーチン・ファウラー先生の黒いDSL本(翻訳版)が5月2日に発売されました。遅ればせながら私も最近購入して熟読しているところです。
この本が示すDSLの種類や内容は、あくまでもオブジェクト指向というコンテキストにおいてのものであり、関数型言語によるDSL開発については一切言及はありませんが、まえがきの「本書に欠けていること」の中で「"関数型言語でのDSL"に関する言及はないので、ご了承ください。」というようなお断りがあり、好感が持てます。マーチン・ファウラー先生に限らず、オブジェクト指向の大御所たちも最近の関数型言語流行の流れにはとても敏感になっているようです。実際、ことDSLに関して言うなら、モナディックなパーサ・コンビネータの存在など、確かに関数型言語の方が有利になる点もいくつかあるし、それらについて書籍内に言及がないことを説明するのは良いことだと思う。この本で示されている考え方やパターンについて関数型言語ではどのように考え適用していけばよいのか。自分の中で消化していきたい。そんなこんなで、黒いDSL本が結構人気みたいです。日本のデベロッパーのDSLに対する関心の高まりを感じたり、ドラゴンズドグマが楽しみだったりな今日この頃ですが、いかがお過ごしでしょうか。
アレについて一言だけ言っておこうと思ったけど、黒い本読み終わった後の方がよさげかな。
まだ読み終わっていないんですが書いちゃいます。
この記事では、「F#3.0ではコンピューテーション式が拡張されたので、内部DSLが作りやすくなりましたよ。」という話題を提供します。
言語指向プログラミング(LOP)とは
言語指向プログラミング(LOP)とは、メタプログラミングと同様にひとことで言い表すことは容易ではない抽象的な概念ですが、大きな意味で「ドメイン特化言語(DSL:Domain Specific Language)を使ってソフトウェア構築を行う一般的な開発スタイル」というように具体的に捉えることもできます。言語指向プログラミングを理解するには、まずDSLとは何かと言うことを理解する必要があります。
ある特定のドメイン(目的)の問題解決のために特化させた専用のプログラミング言語のことをDSLと言います。専用言語というとなんだか難しいように聞こえるかもしれませんが、実のところそんなたいした話ではなく、多くの場合はXML等の設定ファイルやライブラリ、あるいはフレームワークの延長上に自然と現れてくるものです*1。あらかじめDSLで開発すること考えて設計をできるのが理想的ですが、少し凝った設定ファイルが拡張を繰り返すたびにいつの間にかDSLのようなものになっていたというケースは現場ではそう珍しいことではないかもしれません。
ごく身近にあるDSLの例として、Excelのセル内の値は「A2」や「D5」などのように、Excel固有の表し方でシンプルに表す機能があります。これは、汎用プログラミング言語のようにデータの型などを記述することなく、「=A2+D5」などのように単純な式において値を計算をすることができます。これは特定の問題に対する専用言語として捉えることができ、つまり一種のDSLであると言えます。この例からもわかるように、DSLの主な利点は特定の問題に対して表現がとてもシンプルになるということです。このように特定の問題に対応するためにホスト言語とは別の言語を定義して、それを用いて特定ドメインの問題を解決しようとする考え方や手法。それを言語指向プログラミングと言います。
DSLには、大きく分けて外部DSLと内部DSLの2種類があり、ホスト言語*2とは異なる言語で作成するものを外部DSL(例えばXMLファイルなどを使用する手法。弾幕記述言語BulletMLなど。)といい、ホスト言語のサブセットで書かれるタイプのものを内部DSLあるいは組み込み型DSL(.NETのLINQなど)と呼びます。言語指向プログラミングで伝統的なものとしては、Unixリトル言語、Lisp、アクティブデータモデル、XML設定ファイルなどがあり、現在も様々な場面で広く活用されています。
言語指向プログラミングおよびDSL開発についてより詳しい情報が知りたい場合は、マーチン・ファウラー先生著の黒いDSL本こと「ドメイン特化言語 パターンで学ぶDSLのベストプラクティス46項目」を読むか、あるいは「LanguageWorkbench - Martin Fowler's Bliki in Japanese」あたりを参照されたい。
F#3.0で加速する言語指向プログラミング(LOP)。コンピューテーション式はもはやモナドだけのための構文ではない!!!
F#(F#2.0)は、強い静的型付き言語としては比較的言語指向プログラミングのやりやすい言語です。パターンマッチやアクティブパターンを利用して抽象的にDSLを定義する手法、XML設定ファイルを読み込んで外部DSLを作成する古典的な手法、コンピュテーション式を用いて計算式として内部DSLを作成する手法。モナディックなパーサ・コンビネータライブラリFParsecを利用して構文解析を行う手法、あるいはfslex/fsyaccを利用したコンパイラの作成など、その方法はさまざまです。
F#3.0ってゆーとTypeProviderが注目されがちだけど、俺的にはあっちの方がよだれ生唾ごっくんものだよ
F#3.0で追加される2つの新機能によって、言語指向プログラミング(LOP)の手法の幅がさらに広がります。その1つは、ご存じTypeProvider。TypeProviderはコード生成と動的型付けの両方に替わるものとして発表当時から注目を集めています。この機能が追加された直接の目的とは異なりますが、外部DSLを作成する手法のひとつとしてTypeProviderが新たに加わりました。
もう1つは、Query expressions(クエリ式)です。クエリ式およびそのクエリメカニズムそのものがDSL作成について直接影響を与えるものではありませんが、新たにクエリ式の機能を提供するにあたって合わせて追加された仕様である「コンピューテーション式に、独自のキーワードを定義することができるカスタムクエリ拡張機能」が大きな影響を与えます。F#2.0ではコンピューテーション式において、 let!、do! 、return、return!などの特定のキーワードのみが利用可能でした。コンピューテーション式は、モナドを書くために限定された機能というわけではありませんでしたが、BindやReturnなどモナドの文脈として利用されるキーワードの色が強く、事実上モナドのための構文として利用されてきました。なぜなら、F#2.0ではコンピューテーション式で利用できるキーワードを拡張する方法が提供されていなかったからです。しかし、F#3.0のコンピューテーション式ではこれが拡張可能であり、CustomOperationAttributeクラスを用いることで独自のキーワードを定義することができ、ある程度柔軟なカスタマイズができます。これは大変エキサイティングなことです!!!
実際どういうことができるのかというと、以下のようなことが可能となります。
type SeqBuilder() = member __.For (source, body) = seq { for v in source do yield! body v } member __.Yield (item)= seq { yield item } [<CustomOperation("select")>] member __.Select (source, f)= Seq.map f source let myseq = SeqBuilder() myseq { for i in 1 .. 10 do select (fun i -> i + 100) } |> Seq.iter (printfn "%d")
この仕組みの詳細については、まだ大々的に公表されているものではありませんが、MSDN - Core.CustomOperationAttribute クラス (F#)にて、ある程度利用方法を把握することができます。F#3.0で言語指向プログラミングが加速するとはつまりこういうことです。コンピューテーション式はもはやモナドだけのための構文ではないのです!!!
ProjectionParameterAttributeの利用
カスタムオペレーションの引数をProjectionParameter属性でマークすると、自動的にパラメーター化(というかカスタムキーワードに続く式を暗黙的に関数に変換)してくれる。type SeqBuilder() = member __.For (source, body) = seq { for v in source do yield! body v } member __.Yield (item)= seq { yield item } [<CustomOperation("select")>] member __.Select (source, [<ProjectionParameter>] f) = Seq.map f source let myseq = SeqBuilder() myseq { for i in 1 .. 10 do select (i + 100) } |> Seq.iter (printfn "%d")
MaintainsVariableSpaceプロパティの利用
CustomOperation属性のMaintainsVariableSpaceプロパティをtrueに設定すると、以下のようにシーケンス内の値を変更せずに維持するカスタムキーワードに設定できる。type SeqBuilder() = member __.For (source, body) = seq { for v in source do yield! body v } member __.Yield (item)= seq { yield item } [<CustomOperation("select")>] member __.Select (source, [<ProjectionParameter>] f) = Seq.map f source [<CustomOperation("reverse", MaintainsVariableSpace = true)>] member __.Reverse (source) = List.ofSeq source |> List.rev let myseq = SeqBuilder() myseq { let x = 1 for i in 1 .. 10 do reverse select (x, i + 100) } |> Seq.iter (printfn "%A")
この他にも、 into の使用をサポートするAllowIntoPatternプロパティや、2つの入力をサポートするIsLikeZipプロパティなど、柔軟な拡張なためのオプションがいくつか用意されている。
サンプル:FizzBuzzBuilder
fizzbuzz { for i in 1..100 do execute 3 5} |> Seq.iter (printfn "%A")
上記のようにFizzBuzzを書けるようにする内部DSLを書いてみましょう。
type FizzBuzzBuilder() = member __.For (source, body) = seq { for v in source do yield! body v } member __.Yield (x) = seq { yield x } [<CustomOperation("select")>] member __.Select (source, [<ProjectionParameter>] f) = Seq.map f source [<CustomOperation("execute")>] member __.Execute (source, a, b) = if a = 0 then invalidArg "fizz" "ゼロだめ" if b = 0 then invalidArg "buzz" "ゼロだめ" let fzbz x = (x%a,x%b) |> function |0,0 -> "FizzBuzz" |0,_ -> "Fizz" |_,0 -> "Buzz" | _ -> string x source |> Seq.map fzbz let fizzbuzz = FizzBuzzBuilder()
サンプル:もっとFizzBuzzBuilder
fizzbuzz { fizz 3 buzz 5 execute [1..100]} |> Seq.iter (printfn "%A")
もうちょっとDSLっぽさを醸し出したいと思います。上記のように記述できるDSLを書いてみましょう。
type FizzBuzzBuilder() = [<CustomOperation("fizz")>] member __.Fizz (_, x) = x,0 [<CustomOperation("buzz")>] member __.Buzz ((x,_), y) = x,y [<CustomOperation("execute")>] member __.Execute ((a,b),source) = if a = 0 then invalidArg "fizz" "ゼロだめ" if b = 0 then invalidArg "buzz" "ゼロだめ" let fzbz x = (x%a,x%b) |> function |0,0 -> "FizzBuzz" |0,_ -> "Fizz" |_,0 -> "Buzz" | _ -> string x source |> Seq.map fzbz member __.Yield (x) = x let fizzbuzz = FizzBuzzBuilder()
凝ったことは何もしていませんが、これまでのF#2.0ではできない表現です。面白いですね。F#3.0のコンピューテーション式は複雑なDSLを作るには向いているとは言えませんが、あまり複雑ではないちょっとしたDSLが必要になった場合は、検討してみる価値が十分にある手法です。
ちなみに、ビルディング関数および、カスタムキーワードは以下ハードコピーのようにVisual Studio 11 Beta上でもちろんシンタックスハイライトされます。
独自に定義したキーワードもちゃんとハイライトされるなんて。すてき!!!
サンプル:ILBuilder
次はもう少し実用的なサンプル。
MSILerな人は、上記のような感じで記述できるDSLが欲しくなるかもしれません。(というか、そういう人たちはおそらくもう既にお手製のものを作っていることでしょうが。)
open System.Reflection.Emit type Stack<'a> = Stack of (ILGenerator -> unit) type Completed<'a> = Completed of (ILGenerator -> unit) type ILBuilder() = [<CustomOperation("ldc_i4_7")>] member __.ldc4_7(x) = Stack(fun ilg -> ilg.Emit(OpCodes.Ldc_I4_7)) [<CustomOperation("ldc_i4_8")>] member __.ldc4_8(Stack f : Stack<int * (int * 'r)>) = Stack(fun ilg -> f ilg; ilg.Emit(OpCodes.Ldc_I4_8)) [<CustomOperation("ldc_i4_0")>] member __.ldc4_0(Stack f : Stack<int * (int * 'r)>) = Stack(fun ilg -> f ilg; ilg.Emit(OpCodes.Ldc_I4_0)) [<CustomOperation("add")>] member __.Add(Stack f : Stack<int * (int * 'r)>) : Stack<int * 'r> = Stack(fun ilg -> f ilg; ilg.Emit(OpCodes.Add)) [<CustomOperation("mul")>] member __.Mul(Stack f : Stack<int * (int * 'r)>) : Stack<int * 'r> = Stack(fun ilg -> f ilg; ilg.Emit(OpCodes.Mul)) [<CustomOperation("ret")>] member __.Ret(Stack f : Stack<int * (int * 'r)>) = Completed(fun ilg -> f ilg; ilg.Emit(OpCodes.Ret)) member __.Yield x = x member __.Run(Completed f : Completed<'a>) : unit -> 'a = let dm = DynamicMethod("", typeof<'a>, [||]) let g = dm.GetILGenerator() g |> f (dm.CreateDelegate(typeof<System.Func<'a>>) :?> System.Func<'a>).Invoke let il = ILBuilder()
かなり適当で且つ大半を割愛しましたが、こんな感じでMSILのDSLとかも作れてしまいます。頑張って真面目に実装したら、MSIL厨歓喜間違いなしです。
RxQueryBuiler
おわりに、非常にクールな内部DSLをご紹介します。
あのReactive ExtensionsをF#でいい感じに記述することができるようになる、RxQueryBuilerです。
When the Reactive Framework meets F# 3.0 - have fun
http://mnajder.blogspot.jp/2011/09/when-reactive-framework-meets-f-30.html
上記記事に掲載されているコードは、若干バージョンが古いもの向けに書かれており、VS11 Betaおよび最新Rx_Experimental-Main(ForkJoinはExperimental版に入ってるので)に対応していないので、少しだけ修正したものを以下に掲載します。具体的な変更箇所は、IObservable<_>.Single()や、IObservable<_>.First()等が、C#およびVBの async/await サポートにより変更となり、代わりに、IObservable<_>.SingleAsync()、IObservable<_>.SingleAsync()を使用するようにしただけです。
open System open System.Reactive.Linq open System.Reactive.Concurrency type RxQueryBuiler() = member this.For (s:IObservable<_>, body : _ -> IObservable<_>) = s.SelectMany(body) [<CustomOperation("select", AllowIntoPattern=true)>] member this.Select (s:IObservable<_>, [<ProjectionParameter>] selector : _ -> _) = s.Select(selector) [<CustomOperation("where", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.Where (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool ) = s.Where(predicate) [<CustomOperation("takeWhile", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.TakeWhile (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool ) = s.TakeWhile(predicate) [<CustomOperation("take", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.Take (s:IObservable<_>, count) = s.Take(count) [<CustomOperation("skipWhile", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.SkipWhile (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool ) = s.SkipWhile(predicate) [<CustomOperation("skip", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.Skip (s:IObservable<_>, count) = s.Skip(count) member this.Zero () = Observable.Empty(Scheduler.CurrentThread) member this.Yield (value) = Observable.Return(value) [<CustomOperation("count")>] member this.Count (s:IObservable<_>) = Observable.Count(s) [<CustomOperation("all")>] member this.All (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool ) = s.All(new Func<_,bool>(predicate)) [<CustomOperation("contains")>] member this.Contains (s:IObservable<_>, key) = s.Contains(key) [<CustomOperation("distinct", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.Distinct (s:IObservable<_>) = s.Distinct() [<CustomOperation("exactlyOne")>] member this.ExactlyOne (s:IObservable<_>) = s.SingleAsync() [<CustomOperation("exactlyOneOrDefault")>] member this.ExactlyOneOrDefault (s:IObservable<_>) = s.SingleOrDefaultAsync() [<CustomOperation("find")>] member this.Find (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool) = s.FirstAsync(new Func<_,bool>(predicate)) [<CustomOperation("head")>] member this.Head (s:IObservable<_>) = s.FirstAsync() [<CustomOperation("headOrDefault")>] member this.HeadOrDefault (s:IObservable<_>) = s.FirstOrDefaultAsync() [<CustomOperation("last")>] member this.Last (s:IObservable<_>) = s.LastAsync() [<CustomOperation("lastOrDefault")>] member this.LastOrDefault (s:IObservable<_>) = s.LastOrDefaultAsync() [<CustomOperation("maxBy")>] member this.MaxBy (s:IObservable<'a>, [<ProjectionParameter>] valueSelector : 'a -> 'b) = s.MaxBy(new Func<'a,'b>(valueSelector)) [<CustomOperation("minBy")>] member this.MinBy (s:IObservable<'a>, [<ProjectionParameter>] valueSelector : 'a -> 'b) = s.MinBy(new Func<'a,'b>(valueSelector)) [<CustomOperation("nth")>] member this.Nth (s:IObservable<'a>, index ) = s.ElementAt(index) [<CustomOperation("sumBy")>] member inline this.SumBy (s:IObservable<_>,[<ProjectionParameter>] valueSelector : _ -> _) = s.Select(valueSelector).Aggregate(Unchecked.defaultof<_>, new Func<_,_,_>( fun a b -> a + b)) [<CustomOperation("groupBy", AllowIntoPattern=true)>] member this.GroupBy (s:IObservable<_>,[<ProjectionParameter>] keySelector : _ -> _) = s.GroupBy(new Func<_,_>(keySelector)) [<CustomOperation("groupValBy", AllowIntoPattern=true)>] member this.GroupValBy (s:IObservable<_>,[<ProjectionParameter>] resultSelector : _ -> _,[<ProjectionParameter>] keySelector : _ -> _) = s.GroupBy(new Func<_,_>(keySelector),new Func<_,_>(resultSelector)) [<CustomOperation("join", IsLikeJoin=true)>] member this.Join (s1:IObservable<_>,s2:IObservable<_>, [<ProjectionParameter>] s1KeySelector : _ -> _,[<ProjectionParameter>] s2KeySelector : _ -> _,[<ProjectionParameter>] resultSelector : _ -> _) = s1.Join(s2,new Func<_,_>(s1KeySelector),new Func<_,_>(s2KeySelector),new Func<_,_,_>(resultSelector)) [<CustomOperation("groupJoin", AllowIntoPattern=true)>] member this.GroupJoin (s1:IObservable<_>,s2:IObservable<_>, [<ProjectionParameter>] s1KeySelector : _ -> _,[<ProjectionParameter>] s2KeySelector : _ -> _,[<ProjectionParameter>] resultSelector : _ -> _) = s1.GroupJoin(s2,new Func<_,_>(s1KeySelector),new Func<_,_>(s2KeySelector),new Func<_,_,_>(resultSelector)) [<CustomOperation("zip", IsLikeZip=true)>] member this.Zip (s1:IObservable<_>,s2:IObservable<_>,[<ProjectionParameter>] resultSelector : _ -> _) = s1.Zip(s2,new Func<_,_,_>(resultSelector)) [<CustomOperation("forkJoin", IsLikeZip=true)>] member this.ForkJoin (s1:IObservable<_>,s2:IObservable<_>,[<ProjectionParameter>] resultSelector : _ -> _) = s1.ForkJoin(s2,new Func<_,_,_>(resultSelector)) [<CustomOperation("iter")>] member this.Iter(s:IObservable<_>, [<ProjectionParameter>] selector : _ -> _) = s.Do(selector) let rxquery = new RxQueryBuiler()
以前、「F#でRxる。よく訓練されたF#ERはコンピューテーション式をつくる。」という記事を書きましたが、F#3.0のカスタムクエリ演算子の登場によって、完全に過去のモノにしてくれました!もういろいろ自由自在ですね。F#でRxなリアクティブプログラマーもこれで勝つる!!!
最後に
大事なことなのでもう一度言っておきますが、F#3.0のコンピューテーション式はもはやモナドだけのための構文ではないのです!!!F#3.0のコンピューテーション式でイケてる内部DSLを作って、どんどん自慢しちゃいましょう。