F# で LINQ 入門 Skip SkipWhile
気が付けば年開けてましたね。今年も F#!F#!していきたいと思います。
open System.Linq let actors = ["ドラえもん"; "のび太"; "しずか"; "スネ夫"; "ジャイアン"] let game player = player |> Seq.iter (printfn "%s がゲームをしています") // 悪いなのび太、このゲームは3人用なんだ actors.Skip(2) |> game // お前のものはおれのもの、おれのものはおれの物 actors.SkipWhile(fun a -> a <> "ジャイアン") |> game
実行結果
> // 悪いなのび太、このゲームは3人用なんだ actors.Skip(2) |> game;; しずか がゲームをしています スネ夫 がゲームをしています ジャイアン がゲームをしています val it : unit = () > // お前のものはおれのもの、おれのものはおれの物 actors.SkipWhile(fun a -> a <> "ジャイアン") |> game;; ジャイアン がゲームをしています val it : unit = () >
UNO で Skip されるときの得も言われぬ悲しみを F# で表現するのが今年の目標の1つです。
F# 3.0 の Query expressions
この記事は F# Advent Calendar 2011 の13回目です。
- ←前回 [twitter:@furuya02] さん「F#によるパケットモニタの作成(WinPcap)」
- →次回 [twitter:@jsakamoto] さん 「F# で正規表現デザイナ ASP.NET MVC アプリを作成する」
はじめに
今年の 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
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:自分こそが初心者だとも
F# で LINQ 入門 Distinct
入門と銘打ってますが、入門しているのはわたしです。Distinct メソッドでは、シーケンス内から重複した要素を取り除いたシーケンスを取得します。
open System.Linq let cart = [ "TaPL", 6119; "SICP", 4830; "TaPL", 6220; "SICP", 4830; "TaPL", 6119;] cart.Distinct() |> Seq.iter (function | name, price -> printfn "%s を %d円で購入します" name price)
実行結果
TaPL を 6119円で購入します SICP を 4830円で購入します TaPL を 6220円で購入します val cart : (string * int) list = [("TaPL", 6119); ("SICP", 4830); ("TaPL", 6220); ("SICP", 4830); ("TaPL", 6119)]
TaPL や SICP を読めるようなレベルになりたいですね。頑張ろう。まず英語が駄目駄目なんですけども。
F# で LINQ 入門 Where
Where はシーケンスに対し抽出を行います。LINQ の中では最も使用されるメソッドの1つではないでしょうか。
open System.Linq (* 年齢不詳の人物もいるため Option *) type person = { name : string; age : Option<int> } (* 公式より抜粋 http://www.guilty-crown.jp/ *) let charactors = { name = "Shu"; age = Some(17) } :: { name = "Gai"; age = Some(17) } :: { name = "Inori"; age = Some(16) } :: { name = "Ayase"; age = Some(17) } :: { name = "Tsugumi"; age = Some(14) } :: { name = "Shibungi"; age = Some(27) } :: { name = "Arugo"; age = Some(17) } :: { name = "Oogumo"; age = None } :: { name = "Fyu-Neru"; age = None } :: { name = "Hare"; age = Some(16) } :: { name = "Yahiro"; age = Some(17) } :: { name = "Souta"; age = Some(17) } :: { name = "Kanon"; age = Some(17) } :: { name = "Arisa"; age = Some(17) } :: { name = "Daryl"; age = Some(17) } :: [] (* 集以外で17歳以下 *) charactors.Where(fun c -> c.name <> "Shu" && c.age.IsSome && c.age.Value <= 17) |> Seq.iter (fun c -> printfn "%s" c.name)
実行結果
Gai Inori Ayase Tsugumi Arugo Hare Yahiro Souta Kanon Arisa Daryl type person = {name: string; age: Option<int>;} val charactors : person list = [{name = "Shu"; age = Some 17;}; {name = "Gai"; age = Some 17;}; {name = "Inori"; age = Some 16;}; {name = "Ayase"; age = Some 17;}; {name = "Tsugumi"; age = Some 14;}; {name = "Shibungi"; age = Some 27;}; {name = "Arugo"; age = Some 17;}; {name = "Oogumo"; age = null;}; {name = "Fyu-Neru"; age = null;}; {name = "Hare"; age = Some 16;}; {name = "Yahiro"; age = Some 17;}; {name = "Souta"; age = Some 17;}; {name = "Kanon"; age = Some 17;}; {name = "Arisa"; age = Some 17;}; {name = "Daryl"; age = Some 17;}]
ひょっとしたら集自身から取り出せるのかもしれないけれど。その展開も悪くは無い。
F# で LINQ 入門 Single SingleOrDefault
職場でシングルベルを迎えそうなSGです。わたし、ひとりぼっち……
open System.Linq let puellaMagi = ["Mami"] puellaMagi.Single() |> printfn "%s" let newPuellaMagi = "Madoka" :: puellaMagi (* ひとりぼっちじゃないのでエラー newPuellaMagi.Single() |> printfn "%s" newPuellaMagi.SingleOrDefault() |> printfn "%s" *) (* SingleOrDefault は空のシーケンスに対してのみ既定値を返す *) [].SingleOrDefault() |> printfn "%s"
実行結果
Mami val puellaMagi : string list = ["Mami"] val newPuellaMagi : string list = ["Madoka"; "Mami"]
ところで、option のデフォルトって、None なのか、null なのかどっちなんだろう……と思って調べてみました。
let list = [Some("111")] let empty = list.Tail empty.SingleOrDefault()
実行結果
val list : string option list = [Some "111"] val empty : string option list = [] val it : string option = None
もう None も恐くない
F# で LINQ 入門 First FirstOrDefault Last LastOrDefault
始まりは肝心ですし終わりが良くなければ失敗だと思います。SGです。インデクサに 0 や hogeList.Count - 1 なんて書いていませんか? 書いてますね、わたしです。
open System.Linq let firstKiss (taste : string[]) = taste.First() |> printfn "初めてのキスは%sの味がした" let lastKiss (taste : string[]) = taste.Last() |> printfn "最後のキスは%sの味だった……" (* 今回は配列 *) let taste = [|"涙"; "苺"; "毒"|] firstKiss taste lastKiss taste let empty = [||] (* シーケンスに要素がないためエラー firstKiss empty lastKiss empty *) let firstKiss' (taste : string[]) = let message (taste : string[]) = match taste.FirstOrDefault() with | null -> "キスもしたことがない童貞諸君乙であります" | aji -> sprintf "初めてのキスは%sの味がした" aji message taste |> printfn "%s" let lastKiss' (taste : string[]) = let message (taste : string[]) = match taste.LastOrDefault() with | null -> "お前はいままでにしたキスの数を覚えているのか?" | aji -> sprintf "最後のキスは%sの味だった……" aji message taste |> printfn "%s" let parameters = [| taste; empty; |] Array.iter firstKiss' parameters Array.iter lastKiss' parameters
実行結果
初めてのキスは涙の味がした 最後のキスは毒の味だった…… 初めてのキスは涙の味がした キスもしたことがない童貞諸君乙であります 最後のキスは毒の味だった…… お前はいままでにしたキスの数を覚えているのか? val firstKiss : string [] -> unit val lastKiss : string [] -> unit val taste : string [] = [|"涙"; "苺"; "毒"|] val empty : 'a [] val firstKiss' : string [] -> unit val lastKiss' : string [] -> unit val parameters : string [] [] = [|[|"涙"; "苺"; "毒"|]; [||]|]
F# で LINQ 入門 ElementAt ElementAtOrDefault
なかなか .NET Framework 2.0 を抜け出せないまま、LINQ なんて使わないし覚えなくていいや……と思っていたらいつの間にか .NET Framework も 4.5 がリリースされようとしているではありませんか。最近はテストメソッドで LINQ を嗜んでいますが、相変わらずプロジェクトは .NET Framework 2.0 のまま。世の中の動きに置いてかれないよう、LINQ のおさらいをしておくことにしました。折角なので F# で。List モジュールがあるのに F# で。
open System.Linq (* 入部した順に並んでいるリスト。入部順はアニメ準拠 *) let mizusawa = ["Chihaya"; "Taichi"; "Kanade"; "Tsutomu"; "Nikuman"] (* n番目に入部した子を取得する *) let nthMember n = let n = n - 1 mizusawa.ElementAt n (* 肉まん君が表示される *) 5 |> nthMember |> printfn "%s" (* まだ入部していない子を表示しようとするとエラー 6 |> nthMember |> printfn "%s" *) (* エラーではなくデフォルト値が欲しいときは ElementAtOrDefault を使用する *) let nthMember' n = let n = n - 1 mizusawa.ElementAtOrDefault n 5 |> nthMember' |> printfn "%s" 6 |> nthMember' |> printfn "%s" (* null はいやん……option を使おう *) let nthMember'' n = let n = n - 1 match mizusawa.ElementAtOrDefault n with | null -> None | name -> Some(name) 5 |> nthMember'' |> printfn "%A" 6 |> nthMember'' |> printfn "%A"
ElementAt メソッドは単体で使う機会は少ないと思います。
LINQ のメソッドを適当に分けていたら 24 回分になってしまいましたが、流行の一人 Advent Calendar ではありません。明日は First と Last の予定。