さて、特別な共用体型である Option 型を見てみましょう。この型は非常によく使われ、便利なので、言語に組み込まれています。

Option 型についてすでに簡単に説明しましたが、基本に立ち返って、型システムにおける Option 型の位置づけを理解しましょう。

欠損値や無効な値を表したいという状況はよくあります。図で表すと、定義域は次のようになります。

int option

これは明らかに何らかの共用体型を使うべき状況です!

F#では、これを Option 型と呼び、 SomeNone の2つのケースを持つ共用体型として定義しています。同様の型は関数型言語でよく見られます。OCamlやScalaでも Option と呼び、Haskellでは Maybe と呼びます。

以下がその定義です。

type Option<'a> =       // ジェネリックな定義を使う  
   | Some of 'a           // 有効な値
   | None                 // 欠損値
重要: これをインタラクティブウィンドウで評価する場合は、その後必ずセッションをリセットして、組み込み型を復元してください。

Option 型は、他の共用体型と同じように使います。構築時には、 Some ケースか None ケースのいずれかを指定します。

let validInt = Some 1
let invalidInt = None

パターンマッチングでは、他の共用体型と同様に、常にすべてのケースをマッチさせる必要があります。

match validInt with 
| Some x -> printfn "有効な値は %A です" x
| None -> printfn "値はNoneです"

Option 型を参照する型を定義する際は、使うジェネリック型を指定する必要があります。これは、山かっこを使って明示的に行うか、型の後に組み込みの option キーワードを使って行います。以下の例は同じ意味です。

type SearchResult1 = Option<string>  // 明示的なC#スタイルのジェネリクス 
type SearchResult2 = string option   // 組み込みの後置キーワード

Option 型の使い方

Option 型は、F#ライブラリで欠損値や無効な値を表すためによく使われています。

たとえば、 List.tryFind 関数は Option 型を返し、検索条件に一致するものがない場合に None ケースを使って示します。

[1;2;3;4]  |> List.tryFind (fun x-> x = 3)  // Some 3
[1;2;3;4]  |> List.tryFind (fun x-> x = 10) // None

タプルとレコードで使ったのと同じ例を再度見て、代わりに Option 型をどのように使えるかを見てみましょう。

// TryParseのタプルバージョン
let tryParseTuple intStr = 
   try
      let i = System.Int32.Parse intStr
      (true,i)
   with _ -> (false,0)  // どの例外でも

// レコードバージョンでは、戻り値を保持する型を作る
type TryParseResult = {success:bool; value:int} 

// TryParseのレコードバージョン
let tryParseRecord intStr = 
   try
      let i = System.Int32.Parse intStr
      {success=true;value=i}
   with _ -> {success=false;value=0}  

// TryParseのOptionバージョン
let tryParseOption intStr = 
   try
      let i = System.Int32.Parse intStr
      Some i
   with _ -> None

// テスト
tryParseTuple "99"
tryParseRecord "99"
tryParseOption "99"
tryParseTuple "abc"
tryParseRecord "abc"
tryParseOption "abc"

これら3つのアプローチの中で、一般的には "option" バージョンが好まれます。新しい型を定義する必要がなく、単純なケースでは None の意味が文脈から明らかだからです。

注: tryParseOption コードは単なる例です。同様の tryParse 関数が.NETコアライブラリに組み込まれているので、代わりにそちらを使うべきです。

Optionの等価性

他の共用体型と同様に、 Option 型には自動的に定義された等価演算子があります。

let o1 = Some 42
let o2 = Some 42

let areEqual = (o1=o2)

Optionの表現

Option 型には優れたデフォルトの文字列表現があります。他の共用体型とは異なり、 ToString() 表現も良好です。

let o = Some 42
printfn "%A" o   // 良い
printfn "%O" o   // 良い

Optionはプリミティブ型だけのものではない

F#の Option は真の第一級型です。つまり、通常の共用体型にすぎないので、どんな型にも使えます。 たとえば、 Person のような複雑な型の Optionint*int のようなタプル型の Optionint->bool のような関数型の Option 、さらには Option 型の Option などを持つことができます。

type OptionalString = string option 
type OptionalPerson = Person option       // 複雑な型のOption
type OptionalTuple = (int*int) option       
type OptionalFunc = (int -> bool) option  // 関数のOption
type NestedOptionalString = OptionalString option // ネストしたOption!
type StrangeOption = string option option option

Option 型の正しい使い方

Option 型は IsSomeIsNoneValue などの関数を使って、パターンマッチングせずに「ラップされた」値にアクセスできますが、これらを使わないようにしましょう!これは慣用的な使い方ではなく、危険で例外を引き起こす可能性があります。

以下は、やってはいけない例です。

let x = Some 99

// IsSomeを使ってテスト
if x.IsSome then printfn "xは %i です" x.Value   // 醜い!!

// まったくマッチングしない
printfn "xは %i です" x.Value   // 醜くて危険!!

そして、こちらが正しい使い方です。

let x = Some 99
match x with 
| Some i -> printfn "xは %i です" i
| None -> () // ここで何をすべきか?

パターンマッチングを行うと、None ケースで何が起こるかを考えたり、ドキュメント化したりする必要が出てきます。 IsSome を使うと、この点を見落としがちです。

Option モジュール

Optionに対して多くのパターンマッチングを行っている場合は、Option モジュールを利用しましょう。このモジュールには、 mapbinditer など、便利なヘルパー関数があります。

たとえば、Optionの値が有効な場合に値を2倍したいとします。パターンマッチングによる方法はこちらです。

let x = Some 99
let result = match x with 
| Some i -> Some(i * 2)
| None -> None

そして、 Option.map を使ったよりコンパクトな書き方がこちらです。

let x = Some 99
x |> Option.map (fun v -> v * 2)

さらに、Optionの値が有効なら2倍して、 None なら0を返したいとします。パターンマッチングによる方法はこちらです。

let x = Some 99
let result = match x with 
| Some i -> i * 2
| None -> 0

そして、同じことを Option.fold を使って一行で書くこともできます。

let x = Some 99
x |> Option.fold (fun _ v -> v * 2) 0

上記のような単純なケースでは、 defaultArg 関数も使えます。

let x = Some 99
defaultArg x 0

Option vs. Null vs. Nullable

Option 型は、C#やその他の言語で null やnull許容型を扱ってきた人たちにとっては混乱の原因になることがよくあります。このセクションでは、それらの違いを説明します。

Optionと null の型安全性

C#やJavaのような言語では、 "null" は存在しないオブジェクトへの参照やポインタを意味します。 "null" はオブジェクトとまったく同じ型を持つので、型システムからは nullになりえるかどうかを判断できません。*

*訳注: C# 8.0以降には「null許容参照型」が追加されているため、オブジェクトがnullになりえるかを型で区別できるようになっています。

たとえば、以下のC#コードでは、有効な文字列を持つ変数と、null文字列を持つ変数の2つの文字列変数を作ります。

string s1 = "abc";
var len1 = s1.Length;

string s2 = null;
var len2 = s2.Length;

これはもちろん、問題なくコンパイルされます。コンパイラは2つの変数の違いを判断できません。 null は有効な文字列と まったく同じ型 なので、System.String のメソッドやプロパティをすべて使えます。 Length プロパティも使えます。

この時点で、このコードがエラーになることは明らかですが、コンパイラは助けてくれません。そのため、皆さんご存知の通り、常に null をチェックする必要が出てきます。

では、上記のC#の例に最も近いF#の例を見てみましょう。F#では、欠損データを示すためにOption 型を使い、 None に設定します。(この例では、わざと明示的に型付けされたNoneを使っていますが、通常は必要ありません。)

let s1 = "abc"
let len1 = s1.Length

// Noneの値を持つstring optionを作る
let s2 = Option<string>.None
let len2 = s2.Length

F#のバージョンでは、コンパイル時エラーがすぐに発生します。 None は文字列ではなく、まったく別の型なので、直接Lengthを呼び出せません。 さらに補足すると、 Some [string] string と異なる型なので、こちらも Length を呼び出せません!

つまり、 Option<string> が文字列ではなく、その中に(もしかしたら)含まれている文字列で何かをしたい場合、パターンマッチングを強制されます(前述の悪い方法を使わない限り)。

let s2 = Option<string>.None

// どちらのケースか?
let len2 = match s2 with
| Some s -> s.Length
| None -> 0

Option<string> 型の値が与えられた場合、それがそれが Some なのか None なのかを判断できないため、常にパターンマッチングを行う必要があります。

同様に、 Option<int>int とは異なる型であり、Option<bool>bool とは異なる型です。

要点をまとめます。

  • string option 型は string 型とまったく異なるものです。 string option から string へのキャストはできません。両者は同じプロパティを持っていません。 string を扱う関数は string option では動きません。その逆も然りです。型システムがエラーを防いでくれます。
  • 一方、C#の「null文字列」は string 型とまったく同じ型です。コンパイル時には区別できず、実行時にのみ判断できます。「null文字列」は有効な文字列と同じプロパティや関数を持っているように見えますが、使おうとするとコードがクラッシュします!

Nullと欠損データ

C#で使うnullは、あらゆるシステムのモデル化において、言語にかかわらず有効な概念である「欠損」データの概念とはまったく異なります。

真の関数型言語では、欠損データの概念は存在しますが、「null」のようなものは存在しません。「ポインタ」や「初期化されていない変数」の概念は関数型の考え方には存在しないからです。

たとえば、次のような式の結果に束縛された値を考えてみましょう。

let x = "hello world"

この値が初期化されていない状態になったり、nullになったり、あるいは他の値になったりするでしょうか?

残念ながら、APIの設計者が「欠損」データの概念を示すためにnullを使ったケースもあり、混乱が生じています! たとえば、.NETライブラリの StreamReader.ReadLine メソッドは、ファイルにこれ以上データがないことを示すためにnullを返します。

F#とnull

F#は純粋な関数型言語ではなく、nullの概念を持つ.NET言語と相互作用する必要があります。そのため、F# の設計には null キーワードが含まれていますが、使いにくく異常な値として扱われます。

一般的なルールとして、「純粋な」F#ではnullは決して作られず、.NETライブラリや他の外部システムとの相互作用でのみ現れます。

以下に例を示します。

// 純粋なF#型は(一般的に)nullにはできません
type Person = {first:string; last:string}  
let p : Person = null                      // エラー! 

// CLRで定義された型なので、nullが許可されています
let s : string = null                      // エラーなし! 
let line = streamReader.ReadLine()         // nullでもエラーなし

このような場合、すぐにnullをチェックして Option 型に変換するのが良い習慣です!

// streamReaderの例
let line = match streamReader.ReadLine() with
           | null -> None
           | line -> Some line

// 環境変数の例
let GetEnvVar var = 
    match System.Environment.GetEnvironmentVariable(var) with
    | null -> None
    | value -> Some value

// 試してみる
GetEnvVar "PATH"
GetEnvVar "TEST"

また、場合によっては、外部ライブラリに null を渡す必要があるかもしれません。その場合は null キーワードが使えます。

Option vs. Nullable

C#には null に加えて、 Nullable<int> のようなNullable型があり、 Option 型に似ているように見えます。では、どこが違うのでしょうか?

基本的な考え方は同じですが、Nullableははるかに弱いものです。IntDateTime のような値型でのみ動き、文字列やクラス、関数のような参照型では動きません。Nullableをネストすることはできず、特別な振る舞いもほとんどありません。

一方、F#のOptionは真の第一級型であり、すべての型で一貫して同じように使えます(「Optionはプリミティブ型だけのものではない」セクションの例を参照してください)。

results matching ""

    No results matching ""