SG

F# 3.0 の Query expressions

 この記事は F# Advent Calendar 2011 の13回目です。

 

はじめに

 今年の Advent Calendar 向けに、「F# で iPhone 向けゲームを作成する」という記事を書こうと奮闘していましたが、ゲーム(というより 3D)の開発知識を問われたり、残業の理に導かれたりして間に合わなくなってしまいました。最近、仕事の合間に LINQ の復習をしていたので、F# 3.0 からサポートされる予定の「Query expressions」についてゆるふわに書いておこうと思います。

注意

 Developer Preview の F# を使用しているため、将来サポートされる形式がこの通りとは限らないことを予めお伝えしておきます。間違ったことを書いて初心者*1に害を成す恐れがありますので、F# に詳しい人は随時指摘の準備をしておいてください。

Query expressions

 Query expressions(クエリ式) を利用することで、F# でも C#VB のようにキーワードを使用して LINQ によるデータアクセスを行うことが可能となります。

open System.Text.RegularExpressions

type bookData = { Title : string; Authors : string list }

(* F# おすすめ書籍。@wof_moriguchi さんの記事を参考にしました。ありがとうございます *)
let fSharpBooks = [{ Title = "実践F#"; Authors = ["荒井省三"; "いげ太"] }
                   { Title = "プログラミングF#"; Authors = ["Chris Smith"] }
                   { Title = "Beginning F#"; Authors = ["Robert Pickering"; "Don Syme"; "Chance Coble"] }
                   { Title = "Professional F# 2.0"; Authors = ["Ted Neward"; "Aaron Erickson"; "Talbott Crowell"; "Rick Minerich"] }
                   { Title = "Expert F# 2.0"; Authors = ["Don Syme"; "Adam Granicz"; "Antonio Cisternino"] }
                   { Title = "F# for Scientists"; Authors = ["Jon Harrop"; "Don Syme"] }
                   { Title = "Real-World Functional Programming: With Examples in F# and C#"; Authors = ["Tomas Petricek"; "Jon Skeet"] }
                  ]

let foreignBooks = 
    let isAlphabetAndNumber s = Regex.IsMatch(s, @"^[a-zA-Z0-9#.\s]+$", RegexOptions.Compiled)
    query {
        for book in fSharpBooks do
        where (book.Title |> isAlphabetAndNumber)
    }

foreignBooks
|> Seq.iter (fun book -> printfn "F# のお薦め洋書 %s" book.Title)


構文について

 F# のクエリ構文の LINQ は、

query { expression }

 となっています。seq や async のように、組込のコンピュテーション式として実装されています。query の実体は QueryBuilder というクラスのインスタンスです。
 QueryBuilder のメソッドを眺めてみると、Where、Select、All など、C# プログラマーVB プログラマー(俺俺)に馴染みの面々が揃っています。QueryBuilder で定義された関数によって動作を置き換えることにより、クエリ構文による記述をサポートしているようです。

メソッドについて

 上記のコードのとおり、C#VB では、クエリ構文の LINQ は「from 〜」で始まりますが、F# では「for 〜」で始まります。C#VB のクエリ構文では、スペースが異なる程度で名称に大きな差はありませんでした。しかし F# のクエリ構文は C#VB とは異なる単語が使用されているものがいくつか存在します。
 比較のため、一覧を作成してみました。

標準クエリ演算子 F# C# VB
All(Of TSource) all (…) 該当なし Aggregate … In … Into All (…)
Any exists (…) 該当なし Aggregate … In … Into Any()
Average averageBy (…) 該当なし Aggregate … In … Into Average()
Cast(Of TResult) 該当なし from int i in numbers From … As …
Count count 該当なし Aggregate … In … Into Count()
Distinct(Of TSource)(IEnumerable(Of TSource)) distinct 該当なし Distinct
GroupBy groupBy … into group … by または group … by … into … Group … By … Into …
GroupJoin(Of TOuter, TInner, TKey, TResult)(IEnumerable(Of TOuter), IEnumerable(Of TInner), Func(Of TOuter, TKey), Func(Of TInner, TKey), Func(Of TOuter, IEnumerable(Of TInner), TResult)) groupJoin (…) into join … in … on … equals … into … Group Join … In … On …
Join(Of TOuter, TInner, TKey, TResult)(IEnumerable(Of TOuter), IEnumerable(Of TInner), Func(Of TOuter, TKey), Func(Of TInner, TKey), Func(Of TOuter, TInner, TResult)) join (…) join … in … on … equals … From x In …, y In … Where x.a = b.a または Join … [As …]In … On …
LongCount 該当なし 該当なし Aggregate … In … Into LongCount()
Max maxBy 該当なし Aggregate … In … Into Max()
Min minBy 該当なし Aggregate … In … Into Min()
OrderBy(Of TSource, TKey)(IEnumerable(Of TSource), Func(Of TSource, TKey)) sortBy orderby Order By
OrderByDescending(Of TSource, TKey)(IEnumerable(Of TSource), Func(Of TSource, TKey)) sortByDescending orderby … descending Order By … Descending
Select select select Select
SelectMany 複数の for 複数の from 複数の From
Skip(Of TSource) skip 該当なし Skip
SkipWhile skipWhile (…) 該当なし Skip While
Sum sumBy 該当なし Aggregate … In … Into Sum()
Take(Of TSource) take 該当なし Take
TakeWhile takeWhile (…) 該当なし Take While
ThenBy(Of TSource, TKey)(IOrderedEnumerable(Of TSource), Func(Of TSource, TKey)) thenBy orderby …, … Order By …, … Descending
Where where (…) where Where

 調べてみるまで知らなかったのですが、C# には無いキーワードが VB には多く実装されていたのですね。VBラムダ式がアレすぎるせいでしょう。VB では標準クエリ演算子よりもクエリ構文を使用したほうが幸せになれるかもしれません。C# を採用したほうが幸せになれるかもしれません。F#をさい(ry
 F# だけのキーワードも。

標準クエリ演算子 F#
ElementAt nth
First head
FirstOrDefault headOrDefault
Last last
LastOrDefault lastOrDefault
Single exactlyOne
SingleOrDefault exactlyOneOrDefault

 名称が関数型らしい名前になっているような気がしますね。他に Nullable 用の構文が用意されています。F# ならでは。

多段 for

let fSharpEvangelist = 
    query {
        for book in fSharpBooks do
        for author in book.Authors do
        sortBy author
        select author
        distinct
    }

fSharpEvangelist
|> Seq.iter (fun name -> printfn "F# エヴァンジェリスト %s" name)

 C#VB で from を多段に出来るように、F# のクエリ構文でも for を多段にすることが出来ます。

let orenoShelf = [{ Title = "実践F#"; Authors = ["荒井省三"; "いげ太"] }
                  { Title = "プログラミングF#"; Authors = ["Chris Smith"] }
                 ]

let homu =
    query {
        for book  in fSharpBooks do
        for oreno in orenoShelf do
        where (book.Title = oreno.Title)
    }

 このときの homu の型は seq となります。select で射影しないと、タプルになるようです。

nth について

(* 見た感じは 4th ですが、当然添え字なので 5 番目の要素を取得します *)
let query3 =
    query {
        for book1 in fSharpBooks do
        nth 4
    }

query3 |> printfn "%A"

 はまる人はいないと思いますが、nth の始まりは 0 です。はい、誰もはまりませんね。配列を宣言するときの添え字が要素数じゃなくて添え字の最大数とかやめてほしいよね。いや dis っているのは空想上の言語ですよ?

F#にクエリ構文が増えると

 クエリ構文が増えるこということは、(自身を含む)読み手に伝えるための語彙が増えるということ。これにより、プログラマーが処理の意図を示すための選択肢も増えることになります。switch 文で書かれたコードは if 文でも書くことが出来ますが、文脈的には switch 文で書かれた方が読みやすい、というケースのように、クエリ構文で書かれた LINQ のほうが、標準クエリ演算子で書かれた LINQ や Seq モジュールよりも読みやすかったり、伝えたい意図を示すことが出来るケースが存在するのではないかと思います。多分、おそらく、きっと、maybe。逆もまた然りですが、コンテキストに合わせた選択が必要となってくるでしょう。

今後の課題

 需要があればモジュール、標準クエリ演算子、クエリ構文の対比を書いてみようと思います。
 F# の疑問を書くとこわい……じゃなかった優しいお兄ちゃんが答えてくれるらしいので。直接 F# と関係ないですが、VS11DP の fsi が文字化けするんですが、どうすればいいのでしょうか! 今回実行結果が画像なのはそのせいです。

*1:自分こそが初心者だとも