F#で言語指向プログラミング(LOP)のアプローチ。Seqを扱うための簡単なワークフローを作って、ワークフローを味見してみよう。
F#では「ワークフロー」という機能を備えることで、言語指向プログラミング*1のアプローチがし易くなっている。
これは開発者にメタプログラミング、DSLといった柔軟性を備えたライブラリを作成する機会を、
より多く与えてくれるのではないだろうか。この機能が言語に標準的に備わっている意義はとても大きいと思う。
前回のエントリで書いた「F#でStateモナドしてみよう。そうですよね、副作用は怖いですものね。」も
ワークフローの機能を利用したひとつの例で、ワークフローの面白さと可能性を垣間見ることができた。
Seqを扱うための簡単なワークフローを作って、ワークフローを味見してみよう
たとえば、以下のようなSeqを扱うための、ささやかなワークフローと補助的な関数を作ってみる。#light namespace Library1 open System open System.Linq ///SeqBuilder type SeqBuilder() = ///LINQで言うところのEnumerable.SelectManyします。 member this.Bind (ies, f) = Seq.map_concat f ies ///1要素のみのseqを作ります member this.Return x = Seq.singleton x ///Seqの操作をいろいろ提供します。 module SeqLibrary = //SeqBuilderを生成します。 let seq = SeqBuilder () //いわゆるguard関数 let guard = function | true -> Seq.singleton () | _ -> Seq.empty ///LINQのSelectを再定義 let select f source = Enumerable.Select(source,new Func<_,_>(f)) ///LINQのWhereを再定義 let where f source = Enumerable.Where(source,new Func<_,_>(f)) ///LINQのCountを再定義 let count f source = Enumerable.Count(source,new Func<_,_>(f)) ///LINQのMaxを再定義 let max (source:seq<'a>) = Enumerable.Max<'a>(source)
SeqBuilderのBindは、let!(let bang)およびdo!(do bang)計算の式に対して呼び出され、
SeqBuilderのReturnは、return計算の式に対して呼び出されます。
ということを踏まえて、作成したワークフローを利用した下記のコードを見てみましょう。
Seqを扱うためのワークフローと補助的な関数を利用してみる
#light open System open System.Linq open Library1.SeqLibrary let panyn x = x |> (print_any >> Console.WriteLine) //Seqの関数を使った場合 let _ = seq { let! x = let option x = match x > 0 || x <= 0 with | true -> Some x | false -> None {1..10} //from |> Seq.filter (fun a -> a > 4) //where |> Seq.filter (fun a -> a < 8) //where |> Seq.choose (fun a -> option (a * a)) //select return x }|> Seq.fold (+) 0 |> panyn //System.Linqをそのまま使った場合 let _ = seq {let! x = let f = {1..10} //from let w1 = Enumerable.Where (f, (fun a -> a > 4)) //where let w2 = Enumerable.Where (w1, (fun a -> a < 8)) //where let s = Enumerable.Select(w2, (fun a -> a * a)) //select s return x }|> Seq.fold (+) 0 |> panyn //System.Linqの関数を再定義して使った場合 let _ = seq {let! x = {1..10} //from |> where (fun a -> a > 4) //where |> where (fun a -> a < 8) //where |> select (fun a -> a * a) //select return x }|> Seq.fold (+) 0 |> panyn //guard関数を使った場合 let _ = seq {let! a = {1..10} //from do! guard (a > 4) //where do! guard (a < 8) //where return a * a }|> Seq.fold (+) 0 |> panyn let k = Console.ReadKey ()
実行結果
110 110 110 110
独自に作成したワークフローや補助的な関数を用いることで、特定の条件を満たすSeqを生成するためのコードが、非常にシンプルに記述されていることが確認できると思います。
なかでも「System.Linqの関数を再定義して使った場合」と「guard関数を使った場合」については、
とても直感的でわかりやすいコードになっているように思います。
System.Linqの関数と近似な関数がSeqモジュールにも提供はされていますが、
Seq.chooseがoption型を要求する点で異なっていて、この場合若干の扱いにくさを感じました。
また、そのままSystem.Linqを利用した場合、パイプでつないで書くことができないのでF#的にはどうも具合が悪いです。
この例では、再定義したSystem.Linqを使って記述するかguard関数を用いるかは、好みがわかれるところかもしれません。
MSDN - ワークフロー(F#)
http://msdn.microsoft.com/ja-jp/library/dd233182(VS.100).aspx
F# の基本を越えて - ワークフロー
http://www.infoq.com/jp/articles/pickering-fsharp-workflow
新たな関数型言語「F#」
http://codezine.jp/article/detail/4254
*1:LOP:Language Oriented Programming