特集:C#開発者のためのF#入門(後編)

F#言語の基礎文法

bleis-tift
2012/05/10
Page1 Page2

判別共用体

* F#では「判別共用体」が提供されている。C/C++開発者であれば「共用体」という言葉になじみがあるだろうが、C#には共用体そのものが存在しないので、「判別共用体」という言葉だけでは、その内容をうまくイメージできない場合もあるかもしれない。そこで、簡単に補足説明しておこう。共用体とは、同じメモリ領域を複数の型が共有する構造を持つデータ型のことだ。つまり共用体では、複数の型のメンバを定義しておけば、その中の1つの型の値を代入できる。

 F#の判別共用体は、ある種のクラス階層を表すために使える。

 例えば、ファイルとディレクトリを判別共用体で表してみよう。次のコードがその例だ。

// 簡単のため、ファイルはファイル名とサイズのみを持っているとする
type FileSystemEntry =
  | File of string * int
  | Dir of string * FileSystemEntry list
ファイルとディレクトリを判別共用体で表した例
定義した「FileSystemEntry」型を、Dirケースの値の型としても定義しているところがポイント。

 このコードでは、「File」と「Dir」という2つのケースを持つ判別共用体としてFileSystemEntry型が定義されている。File/Dirケースの値の型は、「of」以下に記載されている。このコード例では、Fileケースが保持する型はstring型とint型であり、Dirが保持する型はstring型とFileSystemEntry型リストである(タプルの型のように見えるが、実はタプルそのものではない。単に、複数の型を持つことを意味する)。値の型の中で「FileSystemEntry型リスト」が使われることで、再帰的に判別共用体を格納できるので、ファイル・システムのツリー構造を表現できるというわけだ。

 これは、クラス階層で表す場合に比べ、非常にシンプルに定義できている。

 判別共用体はパターン・マッチとともに使うことが多いため、判別共用体の使い方などは後に回すとして、ここではいろいろな判別共用体を紹介する。

 まず、すでに紹介したリストは、判別共用体として定義されている(次のコードを参照)。

(* F#のリストは言語組み込みで提供されているため
   このコードはコンパイルできない *)
type list<'a> =
  | ([])
  | (::) of 'a * 'a list
リストの定義内容:判別共用体

 つまりF#のリストは、空リスト([])か、もしくは1つの要素とリストをダブルコロン(::)で連結したものといえる。

 今まで記述してきたような、要素をセミコロン(;)で区切ったリストの表記は、実はコード記述を簡易にするために用意されたシンタックス・シュガーである。リストは以下のように、いろいろな書き方が可能だ。

let xs = [1; 2; 3; 4]   // 今までの書き方
let ys = 1::2::3::4::[] // シンタックス・シュガーを使わない書き方
let zs = 1::[2; 3; 4]   // もちろん、「::」の右側にはリストが書ける
いろいろなリストの記述方法

 以上のことから、F#のリストは、

  • 要素を先頭に追加したリストを作成するのは高速(O(1))
  • 要素を末尾に追加したリストを作成するのは低速(O(n))
  • リスト同士を連結したリストを作成するのは低速(O(n))

という特徴を持つことが分かる。

 次に、オプション(option)を見てみよう。

 option型は次のような判別共用体として定義されている。

type option<'a> =
  | None
  | Some of 'a
オプションの定義内容:判別共用体

 オプションとは、含めることのできる要素がただ1つに制限されたリスト、と考えてもらっても構わない。

 F#ではこのoption型を使って「値があるかもしれないし、ないかもしれない」ことを表す。C#などでは、そういう場合には普通、「null」を使うが、そうせずにoption型を使うのには理由がある。

 最も厄介なのは、nullというのは(null許容型を除くと)型に現れないことだ。nullは参照型であればどんな型の値にもなり得る。そのため、null参照によるエラーというのは実行してみるまで分からない。これは、nullが来るかどうかの判定をプログラマーが抜け・漏れなく正確に管理する必要があるということを意味する。

 それに対してoption型は「値がないかもしれない」ということを型として明示できる。さらに、string option型と、単なるstring型は全く別の型であるため、そのままではstring型として使えない(オプションを要素が1つだけ入るリストと考えれば分かりやすいだろう)。そして、string option型の変数から、中に含まれている値を取り出す場合は、常に値がない可能性を考慮する必要がある。

 これらの特徴により、F#ではnullよりも安全に「値がないかもしれない」という事象を扱うことができる。次のコードは、string option型の変数を、string型として扱おうとしているコード例だ。このコードはコンパイル・エラーになる。

let x = Some "hoge" // string option型の変数「x」
// これはコンパイル・エラー
//let ans = x.IndexOf("o")
string option型の変数を、string型として扱おうとしてエラーになるコード例

 では、どのようにして、string option型の変数から値を取り出すのかを見てみよう。

パターン・マッチ

 まずはoption型を例に、match式という構文を見てみよう。

let x = Some "hoge"
let ans =
  match x with
  | Some str -> str.IndexOf("o")
  | None -> -1
printfn "%d" ans;;  // 1
オプションの値を取り出すmatch式のコード例
このコードは、「hoge」という文字列値を含むオプション「x」(=string option型の変数)から、match式を使って文字列を取り出し、その文字列値における「o」が最初に現れるインデックスを取得している。このため、結果は「1」になる。「Some str」ケースは、オプション「x」に値が含まれていた場合を表している(変数「str」に、オプション「x」に含まれる文字列値が渡されている)。それに対して、「None」ケースは、オプション「x」に値が含まれていなかった場合を表している。「->」は、「ならば」と読むといいだろう。

 このコードで、Noneケース(=オプション「x」が値を保持していないケース)を消してみると、「Noneケースの場合を考慮してないよ」という意味の警告が表示される。match式はパターンの網羅性をチェックしてくれるのだ。

 このように、オプションが保持する値を取り出すためには、match式を使って常に値がない場合も扱うことを強制される。判別共用体はオプションに限らず、このmatch式によって各ケースが含む値を取り出すことが多い。

 F#のリストも判別共用体なので、match式によって値をばらすことができる。

let rec sum xs =
  match xs with
  | x::y::rest -> x + y + (sum rest)
  | x::xs -> x + (sum xs)
  | [] -> 0
リストの値を取り出すmatch式のコード例
リストは、シンタックス・シュガーを使わずに記述すると「1::2::3::[]」のようになると説明したが、その理解がここで必要になる。リスト「xs」を引数に取る関数「sum」は再帰関数として定義されている。リストに含まれる値を「x::y::rest」/「x::xs」/「[]」という3つのケースで処理している。「x::y::rest」ケースでは、リストの1番目と2番目の値と、「残りの値を再帰関数『sum』の呼び出した結果の値」を足し算する処理を行っている。「x::xs」ケースでは、リストの1番目と、「残り1つの値を再帰関数『sum』の呼び出し結果の値」に足し算する処理を行っている。リストの場合、残り1つは「[]」という値になるが、[]ケースで「0」の値が返される。これにより、「1+2+(「3::[]」という引数を渡して再帰関数『sum』の呼び出し)」→「3+(「[]」という引数を渡して再帰関数『sum』の呼び出し)」→「0」という流れで、リストの全要素の値が足し算される。

 このコードでは、要素がある場合とない場合に分けて、整数リストの合計を求めている(「x::xs」の部分で、引数の「xs」をシャドウイング(=隠ぺい)している点に注意)。

 「パターン・マッチ」というと、このmatch式を思い浮かべる人も多いが、パターンが書ける場所はmatch式だけではない。F#では、至る所でパターンを書くことができる。

 実は、これまでにもすでにパターン・マッチを使っている部分がある。それは、タプル形式による多引数関数の引数部分だ。

let f (x, y) = x + y
タプル形式による多引数関数の引数部分におけるパターン・マッチ

 これは、関数「f」の引数に対してパターン・マッチを行っているのである。つまりタプル形式による多引数関数は、実は多引数ではなく、タプルを引数の位置で分解しているだけの関数である。上の関数は、次のように書いても同じ意味となる。

// このように書くともはや1引数関数としか見えない
let f xy =
  match xy with
  | (x, y) -> x + y
先ほどの多引数関数を、match式を使って1引数関数として書き直した例

 タプルの形になっているこのパターンを「タプル・パターン」と呼ぶ。タプル・パターンを使ったFizzBuzz問題を解くプログラムは以下のようになる。

let fizzBuzz x =
  match (x % 3, x % 5) with
  | (0, 0) -> "fizz buzz"
  | (0, _) -> "fizz"
  | (_, 0) -> "buzz"
  | _ -> string x
[1..100] |> List.map fizzBuzz |> List.iter (printfn "%s");;
タプル・パターンを使ったFizzBuzz問題を解くプログラム
「[1..100]」は1〜100の数値を要素に持つリストを意味する。「..」は範囲演算子と呼ばれる。「|>」は、前回説明したように、メソッド・チェーンのように左から右へと書き下すパイプライン演算子。

 このコードでは、アンダースコア(_)を使っているが、アンダースコアはどんなものでもマッチするため、「ワイルドカード・パターン」と呼ばれる。「0」という数値を書いている部分は、「定数パターン」と呼ばれる。ここでは、タプル・パターンとワイルドカード・パターン、タプル・パターンと定数パターンのように、パターンをネストしている。

 もう少し分かりやすいパターンのネストの例を見てみよう。

// (int option * int option) listの先頭のタプルが両方ともSomeの場合、格納している値を加算
// そうでない場合、0を返す関数
let f xs =
  match xs with
  | (Some x, Some y)::_ -> x + y
  | other -> 0

printfn "%d" (f [(Some 10, Some 20); (None, None)]) // 30
printfn "%d" (f [(Some 10, None)])                  // 0
printfn "%d" (f [(None, Some 20)])                  // 0
printfn "%d" (f [(None, None)]);;                   // 0
パターンのネストの例

 このように、ネストは何段階でも可能だ(このコードでは、リストのパターン、タプルのパターン、オプションのパターンをネストしている)。

 ここで、ケースの順番を入れ替えてみてほしい。「『(Some x, Some y)::』のパターンには一致しないよ」という警告が表示されたはずだ。このように、match式はパターンの網羅性だけでなく、パターンの重複も検出してくれる。

 引数なしの関数として紹介したhello関数も、実は定数パターンを使っている(次のコードを参照)。

let hello () =
  System.Console.WriteLine("Hello!")
引数なしのhello関数のコード(再掲):定数パターン

 通常、定数パターンは、関数の引数に使うとパターンの網羅性が確保できないために警告となる。しかし、unit型の値は「()」のみであるため、網羅性のチェックに通るのである。

 ほかにも、ORパターン(=「|」)を使うことでパターンを網羅して引数の位置に定数パターンを置くこともできる(次のコードを参照)。

let f (true|false) = 0
// だがこれに意味などないので、ワイルドカード・パターンで十分だろう
let f (_: bool) = 0
ORパターンを用いた定数パターン/ワイルドカード・パターン

 判別共用体とタプルを分解するパターン・マッチを中心に見てきたが、もちろんレコードを分解することもできる。例えばPerson型のNameフィールドの値を取り出す関数は、

type Person = { Name: string; Age: int }
let name p = p.Name
Person型(レコード)のNameフィールドの値を取り出す関数

のように書けるが、レコードをパターン・マッチで分解して、

// 引数で渡されたPerson型のインスタンスの
// Nameフィールドの値を変数「n」として使えるようにする
let name { Name = n } = n   // 関数本体では「n」の値を返すだけ
Person型(レコード)のをパターン・マッチで分解した場合の、Nameフィールドの値を取り出す関数

と書くこともできる。

 これではあまりありがたみはないが、パターン・マッチは、match式や、関数の引数だけでなく、「let」の部分でも使える。

// 何か複雑な型
type SomeComplexType = {
  Name: string
  Age: int
  Other: string list
  Another: int option
}
「let」の部分でパターン・マッチを使うために用意する複雑な型のコード例

 例えば上記のような型があったとして、あるSomeComplexType型のインスタンス「x」に対し、名前(Nameフィールド)と年齢(Ageフィールド)のみが必要だとしよう。このとき、

let { Name = name; Age = age } = x
「let」の部分でパターン・マッチを使うコード例

とすることで、必要なもののみをパターン・マッチにより抜き出せる。

 もちろん、名前が必要な部分で「x.Name」、年齢が必要な部分で「x.Age」としてもいいのだが、参照箇所が複数にある場合、「name」や「age」として参照できる方がありがたい。この「let」の部分でのパターン・マッチは、(以下のように)もちろんタプルに対しても有効だ。

// 一括初期化
let (a, b) = (10, 20)
// 値の入れ替え
let (b, a) = (a, b)

// 多値を返す関数の結果を分解して格納
let f x = (x, x * 3)
let (a, b) = f 10
「let」の部分でパターン・マッチを使うコード例(タプルの場合)

 最後に、変数パターンを紹介しよう。

 これはワイルドカード・パターンに似ているが、ワイルドカード・パターンは値を捨てるのに対して、変数パターンではマッチした値を変数として使える。実は変数パターンもすでに使っている。次のコードは変数パターンの例である。

// optionのパターン・マッチで……
let x = Some "hoge"
let ans =
  match x with
    //   ↓の「str」は変数パターン
  | Some str -> str.IndexOf("o")
  | None -> -1

// リストのパターン・マッチで……
let rec sum xs =
  match xs with
  //↓の「x」や「xs」は変数パターン
  | x::xs -> x + (sum xs)
  | [] -> 0

// レコードのパターン・マッチで
//                ↓の「n」は変数パターン
let name { Name = n } = n
変数パターンの例

 これら以外にもいろいろなところで変数パターンは使っている。例えば、パターンのネストの例がそうだ。

// (int option * int option) listの先頭のタプルが両方ともSomeの場合、格納している値を加算
// そうでない場合、「0」を返す関数
let f xs =
  match xs with
  | (Some x, Some y)::_ -> x + y
  | other -> 0  // 上の「x」と「y」だけでなく、「other」も変数パターン
パターンのネストの例における変数パターン

 さて、ここまで来ると、「『let』の位置でパターン・マッチができる」とか、「関数の位置でパターン・マッチができる」(つまり、パターン・マッチは特別なもの)と考えるよりも、「『let』も引数もパターン・マッチしているのだ」と考えた方が自然だ。つまり、

let n = 42  // この「n」は変数パターン
let f x = x // 引数の「x」も変数パターン
「let」も引数も変数パターン

ということだ。このように、F#では変数を導入する箇所では常にパターン・マッチとなる。

 パターン・マッチにはほかにも、ANDパターンやasパターンなど、いろいろなものがあるが、後は書き方と意味を覚えればそのパターンを使うことはできるだろう。

 これまで見たように、パターン・マッチを使うと、値からデータを取り出すコードが素直に書ける。さらに注目してほしいのは、値を作るときと同じ書き方で値を取り出せるという点だ。このように構築と分解を統一した表記で扱えるのも、パターン・マッチの特長だ。

レコードや判別共用体といったユーザー定義型とパターン・マッチの相性はとても素晴らしいものがある。これを意識して、どんどん型を作ってみるといいだろう。

まとめ

 F#で関数型プログラミングをするための最低限の文法を紹介してきたが、いかがだっただろうか。いろいろと見てきたように感じるかもしれないが、前編と後編で紹介した構文的なものは、

  • letキーワードによる変数と関数
  • if式
  • タプル
  • レコード
  • 判別共用体(リストとオプションを含む)
  • パターン・マッチと各種パターン

の6つだけだ。

 このように、関数型言語の基本的な文法はシンプルにできている。F#に限らず、多くの関数型言語はシンプルな構成要素の組み合わせで構築されているのだ。

 ある機能が思ってもみないところで別の機能と関係しており、それが組み合わさって最初は取っ付きにくく感じるかもしれない。しかしこれを逆に考えると、ある程度勉強していけばパズルのピースがどんどんとはまっていくように理解が進むのである。

 この記事で紹介した機能はF#のほんの入り口にすぎないが、これらの道具を基にさらなるF#の世界を探検していってほしい。end of article

 

 INDEX
  特集:C#開発者のためのF#入門(前編)
  F#で初めての関数型プログラミング
    1.F#とは
    2.関数型プログラミングの基礎
    3.リストとタプル
 
  特集:C#開発者のためのF#入門(後編)
  F#言語の基礎文法
    1.主要な文法: if式/letキーワード/レコード
  2.主要な文法: 判別共用体/パターン・マッチ


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間