M.Hiroi's Home Page

F# Programming

お気楽 F# プログラミング超入門

[ PrevPage | F# | NextPage ]

クエリ式

クエリ式 (query 式) は、C# の LINQ (Language Integrated Query) という機能を、F# で利用するためのコンピュテーション式です。LINQ は C# のデータだけではなく、データベースの問い合わせや XML の操作などを統一して行うための仕組みです。C# の LINQ には SQL 同様のクエリ式を使う方法と、標準的なメソッドや演算子を使う方法がありますが、F# のクエリ式は前者と同様の方法になります。今回はクエリ式の基本的な使い方を簡単に説明します。

●クエリ式の構文

クリエ式の構文を以下に示します。

query { expr; ... }

クリエ式はシーケンス (seq 式) と同様に、{ ... } の中に F# の式を記述することができます。さらに、クエリ式専用の「クエリ演算子」が用意されています。たとえば、SQL ではデータの抽出に select 文を使います。

SQL:
select カラム名 form テーブル名 where 条件式;

F# の場合、クエリ式を使うと次のようになります。

F#:
query {
  for 変数 in コレクション do
  where (条件式)
  select 要素
}

for 文でコレクションから要素を取り出して変数にセットします。where は条件を指定し、select は要素を選択します。この動作はシーケンスの if と yield、リストモナドでいえば gurad と return とほぼ同じです。ただし、where で指定する条件式では関数呼び出しができないことに注意してください。上記の例では、クエリ式の返り値はシーケンスとなり、その要素は select で選択した値となります。

簡単な例を示しましょう。

リスト : 生徒の身長表

type student = {id: int; name: string; height: float; rank: int }

let table = [|
  {id=1;  name="Ada";    height=148.7; rank=1}
  {id=2;  name="Alice";  height=149.5; rank=2}
  {id=3;  name="Carey";  height=133.7; rank=3}
  {id=4;  name="Ellen";  height=157.9; rank=4}
  {id=5;  name="Hanna";  height=154.2; rank=1}
  {id=6;  name="Janet";  height=147.8; rank=2}
  {id=7;  name="Linda";  height=154.6; rank=3}
  {id=8;  name="Maria";  height=159.1; rank=4}
  {id=9;  name="Miranda";height=148.2; rank=1}
  {id=10; name="Sara";   height=153.1; rank=2}
  {id=11; name="Tracy";  height=138.2; rank=3}
  {id=12; name="Violet"; height=138.7; rank=4} |]

生徒を表すレコード student を定義します。id は学籍番号、name は名前、height は身長、rank はクラスを表します。このデータから必要なデータを取り出す操作はクエリ式で簡単に行うことができます。

簡単な実行例を示します。

> query { for x in table do select x.name } |> Seq.toList;;
val it: string list =
  ["Ada"; "Alice"; "Carey"; "Ellen"; "Hanna"; "Janet"; "Linda"; "Maria";
   "Miranda"; "Sara"; "Tracy"; "Violet"]

> query { for x in table do select x.height } |> Seq.toList;;
val it: float list =
  [148.7; 149.5; 133.7; 157.9; 154.2; 147.8; 154.6; 159.1; 148.2; 153.1; 138.2;
   138.7]

> query { for x in table do where (x.height > 155.0); select x } |> Seq.toList;;
val it: student list = [{ id = 4
                          name = "Ellen"
                          height = 157.9
                          rank = 4 }; { id = 8
                                        name = "Maria"
                                        height = 159.1
                                        rank = 4 }]

> query { for x in table do where (x.height > 155.0); select (x.name, x.height) } |> Seq.toList;;
val it: (string * float) list = [("Ellen", 157.9); ("Maria", 159.1)]

●基本的なクエリ演算子

選択した要素数は count で求めることができます。

> query { for x in table do select x; count };;
val it: int = 12

> query { for x in table do where (x.height > 155.0); select x; count };;
val it: int = 2

最大値と最小値は maxBy と minBy で求めることができます。

> query { for x in table do maxBy x.height };;
val it: float = 159.1

> query { for x in table do minBy x.height };;
val it: float = 133.7

> query { for x in table do maxBy x.name };;
val it: string = "Violet"

> query { for x in table do minBy x.name };;
val it: string = "Ada"

> query { for x in table do where (x.rank = 1); maxBy x.height };;
val it: float = 154.2

> query { for x in table do where (x.rank = 4); minBy x.height };;
val it: float = 138.7

where で条件を指定することもできます。

選択した要素を昇順にソートするには sortBy を、降順にソートするには sortByDescending を使います。

> query { for x in table do sortBy x.name; select x.name } |> Seq.toList;;
val it: string list =
  ["Ada"; "Alice"; "Carey"; "Ellen"; "Hanna"; "Janet"; "Linda"; "Maria";
   "Miranda"; "Sara"; "Tracy"; "Violet"]

> query { for x in table do sortByDescending x.name; select x.name } |> Seq.toList;;
val it: string list =
  ["Violet"; "Tracy"; "Sara"; "Miranda"; "Maria"; "Linda"; "Janet"; "Hanna";
   "Ellen"; "Carey"; "Alice"; "Ada"]

> query { for x in table do sortBy x.height; select (x.name, x.height) } |> Seq.toList;;
val it: (string * float) list =
  [("Carey", 133.7); ("Tracy", 138.2); ("Violet", 138.7); ("Janet", 147.8);
   ("Miranda", 148.2); ("Ada", 148.7); ("Alice", 149.5); ("Sara", 153.1);
   ("Hanna", 154.2); ("Linda", 154.6); ("Ellen", 157.9); ("Maria", 159.1)]

> query { for x in table do sortByDescending x.height; select (x.name, x.height) } |> Seq.toList;;
val it: (string * float) list =
  [("Maria", 159.1); ("Ellen", 157.9); ("Linda", 154.6); ("Hanna", 154.2);
   ("Sara", 153.1); ("Alice", 149.5); ("Ada", 148.7); ("Miranda", 148.2);
   ("Janet", 147.8); ("Violet", 138.7); ("Tracy", 138.2); ("Carey", 133.7)]

> query { for x in table do sortBy x.name; where (x.rank = 1); select x.name } |> Seq.toList;;
val it: string list = ["Ada"; "Hanna"; "Miranda"]

> query { for x in table do sortBy x.name; where (x.rank = 4); select x.name } |> Seq.toList;;
val it: string list = ["Ellen"; "Maria"; "Violet"]

もちろん、where で条件を指定することができます。

合計値は sumBy で、平均値は averageBy で求めることができます。

> query { for x in table do sumBy x.height};;
val it: float = 1783.7

> query { for x in table do averageBy x.height};;
val it: float = 148.6416667

> query { for x in table do where (x.rank = 1); sumBy x.height };;
val it: float = 451.1

> query { for x in table do where (x.rank = 1); averageBy x.height };;
val it: float = 150.3666667

> query { for x in table do where (x.rank = 2); averageBy x.height };;
val it: float = 150.1333333

> query { for x in table do where (x.rank = 3); averageBy x.height };;
val it: float = 142.1666667

> query { for x in table do where (x.rank = 4); averageBy x.height };;
val it: float = 151.9

●検査

contains item は選択した要素に item が含まれているかチェックします。

> query { for x in table do select x.rank; contains 1 };;
val it: bool = true

> query { for x in table do select x.rank; contains 5 };;
val it: bool = false

find (条件式) は選択した要素から条件式を満たす最初の要素を返します。見つからない場合は例外を送出します。

> query { for x in table do find (x.height > 150.0) };;
val it: student = { id = 4
                    name = "Ellen"
                    height = 157.9
                    rank = 4 }

> query { for x in table do find (x.height < 140.0) };;
val it: student = { id = 3
                    name = "Carey"
                    height = 133.7
                    rank = 3 }

all (条件式) は選択したすべての要素が条件式を満たしていれば真を返します。条件式を満たしていない要素が一つでもあれば偽を返します。exists (条件式) は選択した要素の中で、条件式を満たしている要素が一つでもあれば真を返します。すべての要素が条件式を満たしていない場合は偽を返します。

> query { for x in table do all (x.height > 130.0) };;
val it: bool = true

> query { for x in table do all (x.height > 140.0) };;
val it: bool = false

> query { for x in table do exists (x.height > 155.0) };;
val it: bool = true

> query { for x in table do exists (x.height > 160.0) };;
val it: bool = false

●重複要素の削除

distinct は重複要素を削除します。

> query { for x in table do where (x.height > 150.0); select x.rank } |> Seq.toList;;
val it: int list = [4; 1; 3; 4; 2]

> query { for x in table do where (x.height > 150.0); select x.rank; distinct } |> Seq.toList;;
val it: int list = [4; 1; 3; 2]

> query { for x in table do where (x.height < 140.0); select x.rank } |> Seq.toList;;
val it: int list = [3; 3; 4]

> query { for x in table do where (x.height < 140.0); select x.rank; distinct } |> Seq.toList;;
val it: int list = [3; 4]

> query { for x in [1;2;1;2] do
-         for y in [1;2;3;1;2;3] do
-         select (x, y) };;
val it: seq<int * int> = seq [(1, 1); (1, 2); (1, 3); (1, 1); ...]

> it |> Seq.toList;;
val it: (int * int) list =
  [(1, 1); (1, 2); (1, 3); (1, 1); (1, 2); (1, 3); (2, 1); (2, 2); (2, 3);
   (2, 1); (2, 2); (2, 3); (1, 1); (1, 2); (1, 3); (1, 1); (1, 2); (1, 3);
   (2, 1); (2, 2); (2, 3); (2, 1); (2, 2); (2, 3)]

> query { for x in [1;2;1;2] do
-         for y in [1;2;3;1;2;3] do
-         select (x, y)
-         distinct };;
val it: seq<int * int> = seq [(1, 1); (1, 2); (1, 3); (2, 1); ...]

> it |> Seq.toList;;
val it: (int * int) list = [(1, 1); (1, 2); (1, 3); (2, 1); (2, 2); (2, 3)]

最後の例はシーケンスの要素がタプルになるので、等値の判定はタプルで行われます。つまり (1, 1) と (1, 1) は等しいですが、(1, 1) と (1, 2) は異なる要素になります。

●グループ化

groupBy key into g は選択した要素を指定したキーでグループ化します。g はグループを表す変数です。F# の場合、グループはシーケンスで表されます。

> query { for x in table do groupBy x.rank into g; select g };;
val it: seq<System.Linq.IGrouping<int,student>> =
  seq
    [seq [{ id = 1
            name = "Ada"
            height = 148.7
            rank = 1 }; { id = 5
                          name = "Hanna"
                          height = 154.2
                          rank = 1 }; { id = 9
                                        name = "Miranda"
                                        height = 148.2
                                        rank = 1 }];
     seq [{ id = 2
            name = "Alice"
            height = 149.5
            rank = 2 }; { id = 6
                          name = "Janet"
                          height = 147.8
                          rank = 2 }; { id = 10
                                        name = "Sara"
                                        height = 153.1
                                        rank = 2 }];
     seq [{ id = 3
            name = "Carey"
            height = 133.7
            rank = 3 }; { id = 7
                          name = "Linda"
                          height = 154.6
                          rank = 3 }; { id = 11
                                        name = "Tracy"
                                        height = 138.2
                                        rank = 3 }];
     seq [{ id = 4
            name = "Ellen"
            height = 157.9
            rank = 4 }; { id = 8
                          name = "Maria"
                          height = 159.1
                          rank = 4 }; { id = 12
                                        name = "Violet"
                                        height = 138.7
                                        rank = 4 }]]

> query { for x in table do groupBy x.rank into g; select g.Key };;
val it: seq<int> = seq [1; 2; 3; 4]

キーはプロパティ Key で取得することができます。なお、モジュール Seq にも同様の処理を行う関数 groupBy が用意されています。

●結合演算子

リレーショナルデータベース管理システム (RDBMS) は、複数のテーブルを結合してデータを取得することができます。これを「テーブルの結合」とか「表結合」といいます。F# のクエリ式でも結合演算子が用意されています。

●内部結合

「内部結合 (inner join)」は指定した要素の値が一致するデータだけを取り出す方法です。以下に構文を示します。

query {
  for x in table1 do
  join y in table2 on (x.item1 = y.item2)
  ...
}

table1 の要素の item1 と table2 の要素の item2 を比較して、同じ値のデータを table2 から取り出して table1 に結合します。簡単な例を示しましょう。

> query { for x in [1..5] do
-         join y in [1; 3; 5] on (x = y)
-         select (x, y) };;
val it: seq<int * int> = seq [(1, 1); (3, 3); (5, 5)]

> query { for x in [1..5] do
-         join y in [6..10] on (x = y)
-         select (x, y) };;
val it: seq<int * int> = seq []

最初の例では、[1; 2; 3; 4; 5] と [1;3;5] を join するので、結果は (1, 1), (3, 3), (5, 5) を格納したシーケンスになります。次の例では x と y で同じ値が無いので、結果は空のシーケンスになります。

もう一つ簡単な例を示しましょう。生徒の身長表を 3 つのテーブルに分離します。

リスト : 生徒のデータ

type studentName = {id1: int; name: string}
type studentHeight = {id2: int; height: float}
type studentRank = {id3: int; rank: int}

// 名前
let table1 = [|
  {id1=1;  name="Ada"}
  {id1=2;  name="Alice"}
  {id1=3;  name="Carey"}
  {id1=4;  name="Ellen"}
  {id1=5;  name="Hanna"}
  {id1=6;  name="Janet"}
  {id1=7;  name="Linda"}
  {id1=8;  name="Maria"}
  {id1=9;  name="Miranda"}
  {id1=10; name="Sara"}
  {id1=11; name="Tracy"}
  {id1=12; name="Violet"} |]

// 身長
let table2 = [|
  {id2=1;  height=148.7}
  {id2=2;  height=149.5}
  {id2=3;  height=133.7}
  {id2=4;  height=157.9}
  {id2=5;  height=154.2}
  {id2=6;  height=147.8}
  {id2=7;  height=154.6}
  {id2=8;  height=159.1}
  {id2=9;  height=148.2}
  {id2=10; height=153.1}
  {id2=11; height=138.2}
  {id2=12; height=138.7} |]

// クラス
let table3 = [|
  {id3=1;  rank=1}
  {id3=2;  rank=2}
  {id3=3;  rank=3}
  {id3=4;  rank=4}
  {id3=5;  rank=1}
  {id3=6;  rank=2}
  {id3=7;  rank=3}
  {id3=8;  rank=4}
  {id3=9;  rank=1}
  {id3=10; rank=2}
  {id3=11; rank=3}
  {id3=12; rank=4} |]

join を使うと学籍番号から名前、身長、クラスを簡単に求めることができます。

> query { for x in table1 do
-         join y in table2 on (x.id1 = y.id2)
-         select (x.name, y.height) };;
val it: seq<string * float> =
  seq
    [("Ada", 148.7); ("Alice", 149.5); ("Carey", 133.7); ("Ellen", 157.9); ...]

> it |> Seq.toList;;
val it: (string * float) list =
  [("Ada", 148.7); ("Alice", 149.5); ("Carey", 133.7); ("Ellen", 157.9);
   ("Hanna", 154.2); ("Janet", 147.8); ("Linda", 154.6); ("Maria", 159.1);
   ("Miranda", 148.2); ("Sara", 153.1); ("Tracy", 138.2); ("Violet", 138.7)]

> query { for x in table1 do
-         join y in table3 on (x.id1 = y.id3)
-         select (x.name, y.rank) };;
val it: seq<string * int> =
  seq [("Ada", 1); ("Alice", 2); ("Carey", 3); ("Ellen", 4); ...]

> it |> Seq.toList;;
val it: (string * int) list =
  [("Ada", 1); ("Alice", 2); ("Carey", 3); ("Ellen", 4); ("Hanna", 1);
   ("Janet", 2); ("Linda", 3); ("Maria", 4); ("Miranda", 1); ("Sara", 2);
   ("Tracy", 3); ("Violet", 4)]

最初の例のように、table1 と table2 を join すると、名前と身長を対応付けることができます。table1 と table3 を join すると、名前とクラスを対応付けることができます。

●外部結合

「外部結合 (outer join)」は指定した要素の値が一致するデータだけではなく、どちらかのテーブルにデータが存在すれば、それもいっしょに取り出す方法です。以下に構文を示します。

query {
  for x in table1 do
  leftOuterJoin y in table2 on (x.item1 = y.item2) into result
  for z in result do
  ...
}

leftOuterJoin は条件式 x.item1 = y.item2 を満たさない場合でも、table1 (左側) のデータを取り出します。SQL では left のほかに right や full を指定することができますが、F# のクエリ式では left のみをサポートしています。table2 の結合結果は into の後ろに指定した変数にセットされます。該当データが table2 にない場合、default 値 (値型ならばゼロ値、参照型ならば null) がセットされます。

簡単な例を示しましょう。

> query { for x in [1..5] do
-         leftOuterJoin y in [1; 3; 5] on (x = y) into z
-         for y in z do
-         select (x, y) };;
val it: seq<int * int> = seq [(1, 1); (2, 0); (3, 3); (4, 0); ...]

> it |> Seq.toList;;
val it: (int * int) list = [(1, 1); (2, 0); (3, 3); (4, 0); (5, 5)]

> query { for x in [1..5] do
-         leftOuterJoin y in [6..10] on (x = y) into z
-         for y in z do
-         select (x, y) };;
val it: seq<int * int> = seq [(1, 0); (2, 0); (3, 0); (4, 0); ...]

> it |> Seq.toList;;
val it: (int * int) list = [(1, 0); (2, 0); (3, 0); (4, 0); (5, 0)]

[1; 2; 3; 4; 5] と [1; 3; 5] を leftOuterJoin すると、前者の値はすべて選択されます。後者には 2 と 4 が無いので、x が 2 と 4 の場合、y はゼロ値 (0) になります。[1 .. 5] と [6 .. 10] を leftOuterJoin すると、前者と後者では等しい値が無いので、y の値はすべてゼロ値になります。

もう一つ簡単な例を示しましょう。身長をまだ測っていない生徒がいたとしましょう。これを table4 で表します。

リスト : 未計測データがある場合

// 身長
let table4 = [|
  {id2=1;  height=148.7}
  {id2=2;  height=149.5}
  {id2=3;  height=133.7}
  // {id2=4;  height=157.9}
  {id2=5;  height=154.2}
  {id2=6;  height=147.8}
  {id2=7;  height=154.6}
  // {id2=8;  height=159.1}
  {id2=9;  height=148.2}
  {id2=10; height=153.1}
  {id2=11; height=138.2}
  {id2=12; height=138.7} |]

table1 と table4 を外部結合すると、次のようになります。

> open Height;;
> query { for x in table1 do
-         leftOuterJoin y in table4 on (x.id1 = y.id2) into z
-         for y in z do
-         select (x, y) };;
val it: seq<studentName * studentHeight> =
  seq
    [({ id1 = 1
        name = "Ada" }, { id2 = 1
                          height = 148.7 });
     ({ id1 = 2
        name = "Alice" }, { id2 = 2
                            height = 149.5 });
     ({ id1 = 3
        name = "Carey" }, { id2 = 3
                            height = 133.7 }); ({ id1 = 4
                                                  name = "Ellen" }, null); ...]

studentHeight はレコードなので参照型です。Ellen は身長を計測していないので、y の値は null になります。データに null が含まれていると、データの操作が面倒になります。そこで、未計測の場合は null ではなく、{id2=0, height=0.0} を挿入することにしましょう。

一番簡単な方法はレコードではなく構造体または値型のレコードを使うことです。レコードに属性 [<Struct>] を指定すると、そのレコードは値型になります。つまり、{id: int; height: float} のゼロ値は {id2=0, height=0.0} になります。もう一つは Seq.map を使って null を変換することです。次のリストを見てください。

リスト : null の変換

let checkNull value =
  match box value with
    null -> {id2=0; height=0.0}
  | _    -> value

let a = query {
  for x in table1 do
  leftOuterJoin y in table4 on (x.id1 = y.id2) into result
  for y in Seq.map checkNull result do
  select (x, y)
}

関数 checkNull は引数 value が null であれば {id2=0; height=0.0} を、そうでなければ value をそのまま返します。なお、null とレコードの等値を判定することはできません。null と obj 型であれば等値の判定は可能です。そこで、演算子 box を使って引数を obj 型にアップキャストしています。

> let b = {id2 = 0; height = 0.0};;
val b: studentHeight = { id2 = 0
                         height = 0.0 }

> b = null;;
=> エラー

> let c = box b;;
val c: obj = { id2 = 0
               height = 0.0 }

> c = null;;
val it: bool = false

> box null = null;;
val it: bool = true

あとは、Seq.map に checkNull を渡して、result に含まれている null を変換するだけです。実行結果は次のようになります。

> Seq.toList a;;
val it: (studentName * studentHeight) list =
  [({ id1 = 1
      name = "Ada" }, { id2 = 1
                        height = 148.7 });
   ({ id1 = 2
      name = "Alice" }, { id2 = 2
                          height = 149.5 });
   ({ id1 = 3
      name = "Carey" }, { id2 = 3
                          height = 133.7 });
   ({ id1 = 4
      name = "Ellen" }, { id2 = 0
                          height = 0.0 });
   ({ id1 = 5
      name = "Hanna" }, { id2 = 5
                          height = 154.2 });
   ({ id1 = 6
      name = "Janet" }, { id2 = 6
                          height = 147.8 });
   ({ id1 = 7
      name = "Linda" }, { id2 = 7
                          height = 154.6 });
   ({ id1 = 8
      name = "Maria" }, { id2 = 0
                          height = 0.0 });
   ({ id1 = 9
      name = "Miranda" }, { id2 = 9
                            height = 148.2 });
   ({ id1 = 10
      name = "Sara" }, { id2 = 10
                         height = 153.1 });
   ({ id1 = 11
      name = "Tracy" }, { id2 = 11
                          height = 138.2 });
   ({ id1 = 12
      name = "Violet" }, { id2 = 12
                           height = 138.7 })]

null が {id2 = 0; height = 0.0} に変換されていますね。これで、まだ計測していない人数や名前を簡単に調べることができます。また、計測した人の平均値も求めることができます。

> query { for (x, y) in a do where (y.height = 0); count };;
val it: int = 2

> query { for (x, y) in a do where (y.height = 0); select x.name };;
val it: seq<string> = seq ["Ellen"; "Maria"]

> query { for (x, y) in a do where (y.height > 0); averageBy y.height };;
val it: float = 146.67

●groupJoin

groupJoin は groupBy と同じように、グループ化するときに便利な結合演算子です。groupJoin の構文を以下に示します。

query {
  for x in table1 do
  groupJoin y in table2 on (x.item1 = y.item2) into result
  for y in result do
  ...
}

table1 の要素 item1 がキーとなり、それと等しい table2 の要素 item2 をグループ化します。結合結果は into の後ろの変数にセットされます。groupBy のように、同じグループの要素をシーケンスに格納することはありません。次の例を見てください。

> query { for x in [1..3] do
-         groupJoin y in [1; 1; 2; 1; 2; 3] on (x = y) into result
-         for y in result do
-         select (x, y) };;
val it: seq<int * int> = seq [(1, 1); (1, 1); (1, 1); (2, 2); ...]

> it |> Seq.toList;;
val it: (int * int) list = [(1, 1); (1, 1); (1, 1); (2, 2); (2, 2); (3, 3)]

キーを格納する table1 は [1; 2; 3] で、結合する table2 は [1; 1; 2; 1; 2; 3] です。この場合、キーと等しいデータを table2 から選択し、それが終わったら次のキーと等しいデータを table2 から選択する、という動作になります。したがって、select (x, y) の結果は (1, 1), (1, 1), (1, 1), (2, 2), (2, 2), (3, 3) となります。なお、leftOuterJoin とは違って、キーと等しいデータが table2 にない場合、そのキーは選択されません。ご注意くださいませ。

もう一つ簡単な例として、クラスの名簿を作ってみましょう。次の例を見てください。

> query { for x in [1..4] do
-         groupJoin y in table3 on (x = y.rank) into result
-         for y in result do
-         join z in table1 on (y.id3 = z.id1)
-         select (x, z.name) };;
val it: seq<int * string> =
  seq [(1, "Ada"); (1, "Hanna"); (1, "Miranda"); (2, "Alice"); ...]

> it |> Seq.toList;;
val it: (int * string) list =
  [(1, "Ada"); (1, "Hanna"); (1, "Miranda"); (2, "Alice"); (2, "Janet");
   (2, "Sara"); (3, "Carey"); (3, "Linda"); (3, "Tracy"); (4, "Ellen");
   (4, "Maria"); (4, "Violet")]

クラスは 4 つあるので、最初のテーブルは [1..4] になります。これがキーになります。これと rank を格納したテーブル table3 を groupJoin します。条件式は x = y.rank になります。名前を求める場合、groupJoin の結果である result と名前を格納した table1 を join します。条件式は y.id3 = z.id1 になります。最後に select で x と z.name を選択します。これでクラスの名簿を求めることができます。

このほかにも、クリエ式には便利なクリエ演算子が用意されています。詳細はリファレンスマニュアル クリエ式 をお読みくださいませ。


null 許容値型

.NET には null という特別な値があります。null は「なにも無い」ことを表す値で、C言語の NULL (null pointer) や Java の null、ほかにも Python の None や Ruby の nil など、null のような特別な値を持つプログラミング言語は多数あります。F# の場合、.NET (C#) のライブラリや外部のデータベースを使用するときに null が返されることがあるので、F# にも null が定義されています。今回は null と null 許容値型について簡単に説明します。

●null

F# の場合、C# と同様に null はそのまま記述します。

> null;;
val it: 'a when 'a: null

when 'a: null を「null 制約」といいます。F# のドキュメント 制約 - F# | Microsoft Docs から引用します。

『規定される型では、NULL リテラルがサポートされている必要があります。これには、すべての .NET オブジェクト型が含まれます。ただし、F# リスト、タプル、関数、クラス、レコード、共用体型は除きます。』

大雑把に言うと「その型の変数に null をセットできること」が条件になります。たとえば、F# の文字列は .NET のライブラリ System.String のオブジェクトなので、string 型の変数に null をセットすることが可能です。

> let a: string = null;;
val a: string = <null>

> a = null;;
val it: bool = true

> a = "";;
val it: bool = false

> let b: obj = null;;
val b: obj = <null>

> b = null;;
val it: bool = true

> b = box 1;;
val it: bool = false

null 制約を満たしている型は、演算子 =, <> で null と等値を判定することができます。たとえば、入力ストリームのメソッド ReadLine() は 1 行読み込みますが、ファイルの終端 (EOF) に到達した場合は null を返します。ReadLine() の返り値と null を比較することで、EOF を検出することができます。

obj は .NET のライブラリ System.Object の型略称なので、obj 型の変数に null をセットすることができます。前回説明したように、F# の値を obj にアップキャストすれば、null と等値を判定することができます。

F# のクラスやレコードなどは null 制約を満たしていないので、変数に null をセットすることはできません。

> type Foo() = class end;;
type Foo =
  new: unit -> Foo

> let c: Foo = null;;

  let c: Foo = null;;
  -------------^^^^

=> エラー : 型 'Foo' に 'null' は使用できません

属性 [<AllowNullLiteral>] を設定すると、null 制約を満たすクラスを定義することができます。

> [<AllowNullLiteral>]
- type Bar () = class end;;
type Bar =
  new: unit -> Bar

> let d: Bar = null;;
val d: Bar = <null>

> d = null;;
val it: bool = true

> Bar() = null;;
val it: bool = false

> Bar() <> null;;
val it: bool = true

なお、F# はなるべく null を使わずにすむよう設計されています。通常のプログラムであれば、この属性を設定する必要はほとんどないと思います。

●null 許容値型

.NET (C#, F#) の場合、null は値型の変数にセットすることはできません。値型でも null を扱うことができるように拡張した型を「null 許容値型」といいいます。実体は .NET のジェネリッククラス System.Nullable<'T> です。

open System
let 変数名 = Nullable<型> 値

<型> を省略して F# の型推論に任せることもできます。

> open System;;
> let a = Nullable 123;;
val a: Nullable<int> = 123

Nullable が保持している値を書き換えることはできません。mutable な変数を用意して、その値を書き換えることはできます。

> let mutable b = Nullable 10;;
val mutable b: Nullable<int> = 10

> b <- Nullable 20;;
val it: unit = ()

> b;;
val it: Nullable<int> = 20

> b <- null;;
=> エラー

> b <- Nullable();;
val it: unit = ()

> b;;
val it: Nullable<int> = <null>

Nullable の変数に null をセットするときは Nullable() を使います。値のチェックはプロパティ HasValue を、値はプロパティ Value で求めることができます。HasValue は Nullable の値が null であれば偽を返します。Value は Nullable の値が null であれば例外を送出します。

> a.HasValue;;
val it: bool = true

> a.Value;;
val it: int = 123

> b.HasValue;;
val it: bool = false

> b.Value;;
=> エラー

●null 許容演算子

モジュール Microsoft.FSharp.Linq には null 許容値型を操作するための演算子が用意されています。これを「null 許容演算子」といいます。

a ?op  b,  左辺が null 許容値型
a  op? b,  右辺が null 許容値型
a ?op? b,  両辺が null 許容値型

Nullable の値が null の場合、比較演算子は false になり、算術演算子は値が null の Nullable になります。op は等値演算子、比較演算子、四則演算と剰余演算になります。

=, <>, <, >, <=, >=, +, -, *, /, %

簡単な例を示しましょう。

> a;;
val it: Nullable<int> = 123

> b;;
val it: Nullable<int> = <null>

> a ?> 100;;
val it: bool = true

> a ?< 100;;
val it: bool = false

> b ?< 100;;
val it: bool = false

> b ?> 100;;
val it: bool = false

> a ?=? a;;
val it: bool = true

> a ?=? b;;
val it: bool = false

> a ?+ 100;;
val it: Nullable<int> = 223

> a ?- 100;;
val it: Nullable<int> = 23

> a ?* 100;;
val it: Nullable<int> = 12300

> a ?/ 100;;
val it: Nullable<int> = 1

> b ?+ 100;;
val it: Nullable<int> = <null>

> a ?+? b;;
val it: Nullable<int> = <null>

●Nullable 対応のクエリ演算子

null 許容値型と null 許容演算子は、クエリ式で .NET のライブラリや外部のデータベースを操作するときに便利です。クエリ式には Nullable に対応するクエリ演算子が用意されています。

averageByNullable
sumByNullable
minByNullable
maxByNullable
sortByNullable
sortByNullableDescending

Nullable の値が null の場合、sumBy と averageBy はゼロ値、minBy と maxBy は無視、sortBy と SortByDescending は一番小さな値として扱われます。簡単な例を示しましょう。

> let xs = [Nullable 4.0; Nullable 2.0; Nullable(); Nullable 1.0; Nullable(); Nullable 6.0];;
val xs: Nullable<float> list = [4.0; 2.0; null; 1.0; null; 6.0]

> query { for x in xs do sumByNullable x };;
val it: Nullable<float> = 13.0

> query { for x in xs do averageByNullable x };;
val it: Nullable<float> = 2.166666667

> 13.0 / 4.0;;
val it: float = 3.25

> 13.0 / 6.0;;
val it: float = 2.166666667

> query { for x in xs do maxByNullable x };;
val it: Nullable<float> = 6.0

> query { for x in xs do minByNullable x };;
val it: Nullable<float> = 1.0

> query { for x in xs do sortByNullable x };;
val it: seq<Nullable<float>> = seq [null; null; 1.0; 2.0; ...]

> it |> Seq.toList;;
val it: Nullable<float> list = [null; null; 1.0; 2.0; 4.0; 6.0]

> query { for x in xs do sortByNullableDescending x };;
val it: seq<Nullable<float>> = seq [6.0; 4.0; 2.0; 1.0; ...]

> it |> Seq.toList;;
val it: Nullable<float> list = [6.0; 4.0; 2.0; 1.0; null; null]

興味のある方はリファレンスマニュアル クリエ式 をお読みくださいませ。


Copyright (C) 2022 Makoto Hiroi
All rights reserved.

[ PrevPage | F# | NextPage ]