C# においてnull許容参照型が導入されれば、.NET開発者によるコードの書き方がasync/await以来最も大きく変化することになる。null許容参照型が正式に利用可能になったなら、この機能を正しく動作させるために、null許容の注釈を付けて更新しなければならないライブラリーが無数にある。そして相互運用性を保証するため、F# も同じように対応する必要がある。
現在の状態
F# は現在、いくつかのバージョンのnull許容性をサポートしている。まず、通常の.NET参照型がある。現在、特定の参照型変数がnullを許容するかどうかをコンパイラーに明確に通知する方法はないため、F# での使用(訳注:通常の参照型でのnullの使用)は推奨できない。
推奨される選択肢は Option<T>
である。 これは“Maybe”型としても知られており、null許容性の概念を型安全に表現する方法である。 イディオム的なF# コードでは、値がnullではない(F# の用語では“None”ではない)ことを確認した後にのみ値を読み取ることができる。 これは通常パターンマッチングによって行われる。例を以下に示す。
match ParseDateTime inputString with
| Some(date) -> printfn "%s" (date.ToLocalTime().ToString())
| None -> printfn "Failed to parse the input."
Option<T>
は、null
の代わりとしてOption<T>.None
が定義されている参照型である。これはメモリーを不要に圧迫してしまう可能性が高いため、 F# 4.5ではValueOption<T>
と呼ばれる構造体ベースの代替手段が作成された。しかし、ValueOption<T>にはいくつかの機能が欠けており、それらの機能はF# 4.6がリリースされるまでは完成しない。
F# 開発者が対処する必要があるもう1つのnull型はNullable<T>
である。これはValueOption<T>
に似ているが、値型のみに制限されている。現在のバージョンのC# およびVBでは、型名に?
記号が付加された形で値型が宣言されていれば、これが使われていることがわかる。
(訳注: F# では)クラスがnull許容としてマークされている可能性もある。ある型がAllowNullLiteral
属性を持つ場合、その型のすべての変数がnull許容として扱われる。特定の型のいくつかの変数だけnullを許容したいが他のものは許容したくない場合、この仕様は問題となりかねない。
設計上の問題点
現状のままでは、F# には基本的な部分に設計上の問題点がある。それは、上に挙げた各種のnull許容性に互換性がないことだ。異なる型のnullの間で変換が必要なだけでなく、それらの動作にも重大な違いがある。たとえば、Option<T>
は再帰が可能なので、Option<Option<Int32>>
を使うことができるが、Nullable<T>
ではできない。これらが混在すると、予期しない問題が発生する可能性がある。
従来の.NET参照型と共に使用すると、Option<T>
は型システムに穴を開けてしまう。通常のF# コードで見ることはないが、C# またはVBの関数では、nullでもNoneでもないにもかかわらず、nullを含んだOption<T>
を作成できてしまう。たとえば、Option<string>.Some(null)
のようになる。F# 以外のライブラリ呼び出しの結果をOption<T>
にラップしたときにも、これが偶然に生まれてしまう可能性がある。F# コンパイラーはこの問題を予期していないため、Option<T>
に含まれる値がNoneではないことを確認した後であっても、null参照例外が発生する可能性がある。
この手の非互換性が現れるもう1つの状況は、CLIMutable
属性である。通常、レコード型は不変(イミュータブル)だが、そのためORMでは利用できない。この属性はその問題を解決するが、新しい問題をもたらしてしまう。レコードが変更可能になってしまうということは、オブジェクトの作成後にnullが入り込む可能性があるということであり、レコードにはnullが含まれていないという前提が崩れてしまう。
C# に影響を受けたnull許容参照型
C# と同様に、F# も後方互換性を必要とする。F# 4.xでコンパイル可能なプログラムはすべてF# 5でもコンパイル可能でないといけない。しかし同時に、ある値がnullになり得るかどうかを示すためにC# の注釈を利用したいという強い要望がある。
現在の計画では、C# とまったく同じように、null許容変数には接尾辞?
を付けて示すことになっている。そしてC# 8と同じように、nullをチェックする前にnull許容変数のメソッドやプロパティを呼び出そうとすると警告が表示される。また同様に、既存のコードがコンパイルに通り続けるように、nullを許容しない変数にnullを代入しても単なる警告になる。
この機能はオプトインになる予定である。新しいプロジェクトではデフォルトでオンになるが、既存のプロジェクトではデフォルトでオフになる。
以下の例はnull許容参照型の提案に提示されたもので、変更される可能性がある。
// let束縛で宣言される型
let notAValue : string? = null
// let束縛で宣言される型
let isAValue : string? = "hello world"
let isNotAValue2 : string = null // null許容性の警告が出る
let getLength (x: string?) = x.Length // xはnullを許容する文字列型のため、null許容性の警告が出る
// 関数のパラメーター
let len (str: string?) =
match str with
| null -> -1
| NonNull s -> s.Length // nullでない結果が束縛されている
// 関数のパラメーター
let len (str: string?) =
let s = nullArgCheck "str" str // nullでない文字列を返す
s.Length // nullでない結果が束縛されている
// let束縛で宣言される型
let maybeAValue : string? = hopefullyGetAString()
// 配列型のシグネチャー
let f (arr: string?[]) = ()
// ジェネリックなコード:'T は参照型に制約されることに注意
let findOrNull (index: int) (list: 'T list) : 'T? when 'T : not struct =
match List.tryItem index list with
| Some item -> item
| None -> null
以上からわかるように、新しい構文は既存のF# パターンとの親和性が高く、Option<T>
に似た形式のパターンマッチングもサポートする。当提案では、使い勝手がよくなり過ぎてしまうために、「F# の利用において、この方式が主流になってしまうことが懸念される」と述べられている。
あるプログラマーはこのようにコメントした:「これが主流にならないわけがない。 書きやすく、パフォーマンスが良く、必要なメモリーが少なくて済むのだから。」
また、以下は一般的なコードやパターンマッチングに追加するヘルパー関数のセットである。
isNull
: 与えられた値がnullかどうか判定する。nonNull
: 値がnullでないことを表明する。値がnullだった場合はNullReferenceException
が上がり、そうでなければその値を返す(C# の!
と同様)。withNull
: 値の型を、nullを通常値の一つとして許容する型に変換する。(|NonNull|)
: パターンの中で使われた場合、マッチする値がnullではないことを表明する。(|Null|NotNull|)
: 与えられた値がnullかどうか判定するアクティブパターン。
完全な関数シグネチャーは提案を参照のこと。
Nullable<T> のサポート
Nullable<T>
がサポートされることも望ましいが、その場合、構文と一貫性について疑問が生じる。null許容参照型は属性が付与された通常の変数である一方、null許容値型は値を条件付きでラップする個別の型である。
その結果、型パラメーターがnullを許容するジェネリックメソッドは、構造体に制約されるか、構造体ではないと制約されるかしない限り定義することができない(『プロジェクトをC# 8とnull許容参照型に対応させる』というタイトルの記事でも同じ問題が発生した)。
上記のヘルパー関数はすべてジェネリックであるため、プロトタイプではisNullVのように値型のバージョンも提供されている。
型推論
型推論は、F# が他のほとんどの.NET言語と比べて最も異なる部分の1つである。VBとC# の型推論はローカル変数に限定されているが、F# はパブリックAPIを含むアプリケーションの広い範囲にわたって型を推論することができる。
null許容参照型で型推論を有効にするには、新しい規則が必要となる。
- 型の等価性を決定する時、参照型のnull許容注釈は無視されるが、注釈が一致しない場合は警告が出力される。
- 型の包摂関係を決定する時、参照型のnull許容注釈は無視されるが、注釈が一致しない場合は警告が出力される(記載予定:正確な仕様を言葉で表現するか、または予期する型と実際の型の組で表現する)。
- メソッドのオーバーロードを解決する時、参照型のnull許容注釈は無視されるが、オーバーロードが確定した後で、引数と戻り値の型について注釈が一致しない場合は警告が出力される。
- 抽象スロットを推論する時、参照型のnull許容注釈は無視されるが、注釈が一致しない場合は警告が出力される。
- メソッドの重複をチェックする時、参照型のnull許容注釈は無視される。
これらのすべてに共通するテーマは、null許容参照型がnullを許容しない型とまったく同じ型であることだ。それによって、この2つの間の不適切な変換はコンパイラエラーではなく、単なる警告となる。
配列、リスト、およびシーケンスの場合、null許容性は先頭の要素をもとに推測される。これが望ましくない場合は、型注釈を明示的に追加できる。
追加の考慮事項
obj
型は常にnull許容と見なされるため、obj?
という型は無効である。
nullを許容しない文字列などの参照型は、デフォルト値を持つとは見なされなくなるため、DefaultValue属性を使用することができなくなる。