F#はMicrosoftのDon Syme氏が開発したプログラミング言語で、命令型、オブジェクト指向、関数型の要素を併せ持つマルチパラダイム言語と位置付けられています。実装はオープンソースのMITライセンスのもと、githubのリポジトリで公開されています。
実行環境は.NET Frameworkおよび.NET CoreやMonoといったマルチプラットフォームに対応していますが、サポート状況については各プラットフォームごとの情報を参照してください(「F# の概要」など)。
Nullableって何ですか?
ここで取り上げるNullableはC#などでも提供されるSystem.Nullable<'T>のことです。これはNull許容型を表す型です。これにより、通常はnullに対応しない型でも、それに対応できるようになります。このような型はSystem.ValueTypeを継承した数値型のほか、EnumやDateTimeなども該当します。
F#でのNullable
F#ではNullableにどう対応しているかを、以下のポイントから探ってみることにします。
- Nullableオブジェクト
- Null許容演算子
- Nullable対応のキャスト
Nullableオブジェクト
Nullable<'T>は'T型の値を持つNullableオブジェクトとして宣言できます。例えばNullable 1は1を持つNullableオブジェクトとなります。
このオブジェクトのHasValueプロパティの値はtrueとなり、何らかの値を持つことがわかります。そしてValueプロパティでその値を得られます。
open System
let mutable n: Nullable<int> = Nullable 1 // Nullablleのインスタンス
n.HasValue // true
n.Value // 1
また、Nullable<int> ()などのように、<'T>で型を示したうえで引数なしで実行して得られたオブジェクトではHasValueの値がfalseとなり、Valueで値を得ようとしてもSystem.InvalidOperationExceptionという例外が発生します。
open System
n <- Nullable<int>()
n.HasValue // false
n.Value // 例外発生「System.InvalidOperationException: Null 許容のオブジェクトには値を指定しなければ なりません。」
デフォルト値の設定
Valueによる例外を発生させないように、GetValueOrDefault()メソッドでデフォルト値を設定できます。引数なしなら0もしくは0.0(数値型による)、引数があれば、それがデフォルト値となります。
open System
let mutable n = Nullable<int>(1) // 引数: 1
n.GetValueOrDefault() // 1
n <- Nullable<int>() // 引数なし
n.GetValueOrDefault() // 0
n.GetValueOrDefault(3) // 3
Nullable型の判定
Nullable<'T>オブジェクトで注意すべき点として、型を調べると'T型と判定されるというのがあります。
open System
let n1 = Nullable 1
n1.GetType() = typeof<int> // true
printfn "%A" <| n1.GetType() // "System.Int32"
そこでコードクォート(<@ ... @>)を使うと、Nullableかどうかと'Tの型それぞれを確認できます。
open System
let n1 = Nullable 1
// Nullable<'T>型かどうかを調べる
<@ n1 @>.Type = typeof<Nullable<int>> // true
printfn "%A" <| <@ n1 @>.Type // "System.Nullable`1[System.Int32]"
// 'T型が何かを調べる
<@ n1 @>.Type.GenericTypeArguments.[0] = typeof<int> // true
printfn "%A" <| <@ n1 @>.Type.GenericTypeArguments.[0] // "System.Int32"
=, <>はデフォルトで対応
Nullableオブジェクトは、デフォルトで=, <>の演算に対応していますが、算術演算や大小の比較演算は対応していません。ですが、Null許容演算子を使えば算術演算や比較演算にも対応します。
open System
(Nullable 2) = (Nullable 2) // true
(Nullable 2) <> (Nullable 3) // true
(Nullable 2) < (Nullable 3) // サポートされない(Null許容演算子で対応)
(Nullable 2) + (Nullable 3) // サポートされない(Null許容演算子で対応)
Null許容演算子
Null許容演算子とは、左辺と右辺のどちらか、あるいはいずれもがNullableオブジェクトの場合に利用できる演算子の総称です。大別すると算術演算と比較演算で提供されており、どれも通常の演算子とNullableオブジェクトに対応する側に?が並ぶ記号で表されます。これらはMicrosoft.FSharp.Linq.NullableOperatorsモジュールで実装されています。
算術演算のNull許容演算子
算術演算のNull許容演算子は、両辺のどちらかがNullable()のときは結果もNullable()となりますが、両辺ともに値を持つNullableオブジェクトであれば、計算の結果を持つNullableオブジェクトを得られます。
open System
open Microsoft.FSharp.Linq.NullableOperators
(* 以下いずれも結果は Nullable 3 *)
(Nullable 1) ?+ 2 // 左辺がNullable
1 +? (Nullable 2) // 右辺がNullable
(Nullable 1) ?+? (Nullable 2) // 両辺がNullable
(* 以下いずれも結果はNullable<int>() *)
Nullable<int>() ?+ 2
1 +? Nullable<int>()
Nullable<int>() ?+? Nullable<int>()
比較演算のNull許容演算子
比較演算のNull許容演算子は、Nullableオブジェクトが持つ値どうしを比較します。結果はbool型です。Nullable<'T>()については、<>や同じ'T型どうしのときtrueとなります。
open System
open Microsoft.FSharp.Linq.NullableOperators
(* 以下いずれもtrue *)
(Nullable 3) ?> 1 // 左辺がNullable
3 >? (Nullable 1) // 右辺がNullable
(Nullable 3) ?>? (Nullable 1) // 両辺がNullable
1 <>? (Nullable<int>()) // 右辺がNullable<int>()
(Nullable<int>()) ?=? (Nullable<int>()) // 両辺がNullable<int>()
Nullable対応のキャスト
Nullable<'T>型から'T型へのキャスト
F#ではキャスト(型変換)を行う関数が提供されていますが、これらはNullableオブジェクトにも対応しています。ただし、処理の内容はValueと等価のNullable<'T> -> 'Tであって、'Tを別の型には変換できません。また、引数がNullable<'T>()のときはSystem.InvalidOperationExceptionという例外が発生します。
open System
Nullable 1 |> int // 1
Nullable<int>() |> int // 例外発生「System.InvalidOperationException: Null 許容のオブジェクトには値を指定しなければ なりません。」
同様の処理を行う関数にはunbox<'T>もあります。こちらは引数がNullable<'T>()のときSystem.NullReferenceExceptionという例外が発生します。
open System
Nullable 1 |> unbox<int> // 1
Nullable<int> () |> unbox<int> // 例外発生「System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。」
Nullable<'T>型が持つ値を別の型へのキャスト
Nullable<'T>が持つ値を別の型に変換するキャスト関数はMicrosoft.FSharp.Linq.Nullableモジュールに含まれており、Nullable.intなどで提供されます。これらはopen Microsoft.FSharp.Linqを実行すると利用できます。
open System
open Microsoft.FSharp.Linq
Nullable 1 |> Nullable.float // Nullable<float> 1.0
Nullable 1 |> Nullable.float |> float // 1.0 (float)
nullなんですか?
実は、Nullable<'T> ()のように引数なしで生成されたNullableオブジェクトはnullとみなされることがあります。実際にdefaultofの戻り値はnullとなります。
open System
open Microsoft.FSharp.Core.Operators.Unchecked
defaultof<Nullable> // null
defaultof<Nullable<int>> // null
defaultof<int> // 0
defaultof<System.DateTime> // 0001/01/01 0:00:00
そのため、Nullableオブジェクトの操作で例外が発生することがあります。
さきほどのunbox<'T>でも、引数がNullable<'T>()のときSystem.NullReferenceExceptionという例外が発生することを紹介しました。
open System
Nullable<int>() |> unbox<int> // 例外発生「System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。」
また、この型を調べようとして、直接GetType()メソッドを実行しても同様の例外が発生してしまいますので注意が必要です。
open System
(Nullable<int>()).GetType() // 例外発生「System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。」
ちなみに、このオブジェクトに対してToString()メソッドを実行すると""(長さ0の文字列)となります。
open System
(Nullable<int>()).ToString() // ""(長さ0の文字列)
[おまけ] 'T option型に変換する型拡張
F#のプログラムでは、結果を得られないかもしれないとき、例外の発生を回避するために'T option(もしくはOption<'T>)という型が使われることがあります。
さきほどのunbox<'T>では引数がNullable<'T>()のとき例外が発生してしまいますが、tryUnbox<'T>という関数では、その際にNoneを返すことで例外の発生を回避します。
tryUnbox<int> (System.Nullable<int>()) // None (int option)
tryUnbox<int> (System.Nullable<int>(1)) // Some 1
そこで、F#の型拡張というしくみを利用して、Nullable aのように値を持つ場合はSome aとし、Nullable<'T> ()のように値を持たないときはNoneとするToOptionメソッドを追加する方法を考えてみました。型拡張による追加メソッドであれば、新たなopenをせずに済み、メソッドで改めて<'T>を指定しなくてもオブジェクトに対して直接実行できるのが便利かと思います。
type System.Nullable<'T when 'T : (new : unit -> 'T)
and 'T :> System.ValueType
and 'T : struct> with
member this.ToOption() =
if this.HasValue then Some this.Value else None
(* 使用例 *)
(System.Nullable 1).ToOption() // Some 1
(System.Nullable<int>()).ToOption() // None
最後に改めてF#におけるNullableについての注意点をまとめると
-
Nullable<'T>型が'T型と判定されることがある -
Nullableオブジェクトがnullとみなされることがある
となります。こちらをご覧いただいた方々の参考になればと思います。