nullのない言語の最良の説明
質問
プログラマーがヌルのエラー/例外について不平を言っているとき、誰かが私たちがnullなしで何をしているのかを尋ねます。
私はオプションタイプのクールさについていくつかの基本的なアイデアを持っていますが、それを最も表現するための知識や言語のスキルはありません。何ですか 素晴らしい その人を指し示すことができる平均的なプログラマーに親しみやすい方法で書かれた以下の説明?
- 参照/ポインターを持っていることの望ましくないことは、デフォルトではめまいになります
- オプションタイプがどのように機能するかを含むオプションはどのように機能しますか?
- パターンマッチングと
- モナディックの包括的な
- メッセージを食べるメッセージなどの代替ソリューション
- (私が逃した他の側面)
解決
ヌルが望ましくない理由の簡潔な要約はそれだと思います 意味のない状態は表現できてはなりません.
ドアをモデル化しているとします。 3つの状態のいずれかになります。開閉、閉じますが、ロック解除され、シャットしてロックされています。今、私はそれを線に沿ってモデル化することができました
class Door
private bool isShut
private bool isLocked
そして、私の3つの状態をこれら2つのブール変数にマッピングする方法は明確です。しかし、これは4番目の望ましくない状態を利用できます。 isShut==false && isLocked==true
. 。私が表現として選択したタイプは、この状態を認めているため、クラスがこの状態に入ることがないことを保証するために精神的な努力を費やさなければなりません(おそらく不変式を明示的にコーディングすることで)。対照的に、私が代数データ型を使用して言語を使用している場合、または定義できる列挙のチェックされた列挙を使用している場合
type DoorState =
| Open | ShutAndUnlocked | ShutAndLocked
その後、定義できます
class Door
private DoorState state
そして、これ以上の心配はありません。タイプシステムは、のインスタンスに対して可能な状態が3つしかないことを保証します class Door
これが入ることです。これは、タイプシステムが優れているものです。コンパイル時にクラスのエラー全体を明示的に除外します。
の問題 null
すべての参照タイプは、通常、そのスペースでこの余分な状態を取得します。これは通常望ましくありません。 a string
変数は、任意のキャラクターのシーケンスである可能性があります。 null
問題ドメインにマップされない値。 a Triangle
オブジェクトには3つあります Point
S、それ自体が持っています X
と Y
値、しかし残念ながら Point
sまたは Triangle
それ自体は、私が働いているグラフドメインなどにとって意味のないこのクレイジーなヌル値かもしれません。
おそらく存在しない値をモデル化するつもりである場合、明示的にそれを選択する必要があります。私が人々をモデル化するつもりであるならば、 Person
があります FirstName
そしてa LastName
, 、しかし、一部の人だけが持っています MiddleName
S、それから私は次のようなことを言いたいです
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
どこ string
これは、非微細なタイプであると想定されています。その後、確立するトリッキーな不変剤はありませんし、予期しないこともありません NullReferenceException
s誰かの名前の長さを計算しようとするとき。タイプシステムは、 MiddleName
その可能性を説明します None
, 、一方、を扱うコード FirstName
そこに値があると安全に想定できます。
たとえば、上記のタイプを使用して、この愚かな機能を作成できます。
let TotalNumCharsInPersonsName(p:Person) =
let middleLen = match p.MiddleName with
| None -> 0
| Some(s) -> s.Length
p.FirstName.Length + middleLen + p.LastName.Length
心配はありません。対照的に、文字列などのタイプのヌル可能な参照を持つ言語で、そして仮定
class Person
private string FirstName
private string MiddleName
private string LastName
あなたは次のようなものを作成することになります
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length + p.MiddleName.Length + p.LastName.Length
入ってくる人のオブジェクトが、すべてが非ヌルであるという不変性を持っていない場合、または
let TotalNumCharsInPersonsName(p:Person) =
(if p.FirstName=null then 0 else p.FirstName.Length)
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ (if p.LastName=null then 0 else p.LastName.Length)
または多分
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ p.LastName.Length
仮定して p
最初の/最後がそこにいることを保証しますが、中央はnullになる可能性があります。または、異なる種類の例外を投げる、または誰が何を知っているかをチェックすることができます。これらすべてのクレイジーな実装の選択肢と、あなたが望んでいない、または必要としないこの愚かな表現可能な価値があるので、それについて考えるべきことがすべてです。
ヌルは通常、不必要な複雑さを追加します。 複雑さはすべてのソフトウェアの敵であり、合理的にはいつでも複雑さを減らすよう努力する必要があります。
(これらの簡単な例でさえ、より複雑さがあることに注意してください。 FirstName
そうすることはできません null
, 、a string
表すことができます ""
(空の文字列)、これはおそらくモデル化するつもりの人名でもありません。そのため、非脆弱な文字列でさえ、私たちが「意味のない価値を表す」ことがまだそうかもしれません。繰り返しになりますが、実行時に、またはタイプシステムを使用して、不変剤と条件付きコードを介してこれを戦うことを選択できます(例: NonEmptyString
タイプ)。後者はおそらく賢明ではない(「良い」タイプは、一般的な操作のセットでしばしば「閉じられている」ことが多く、例えば NonEmptyString
閉じられていません .SubString(0,0)
)、しかし、それは設計空間でより多くのポイントを示しています。一日の終わりには、特定のタイプシステムでは、それが取り除かれるのが非常に優れている複雑さと、本質的に取り除くのが非常に難しい他の複雑さがあります。このトピックの重要なのは、ほぼそれです 毎日 タイプシステム、「デフォルトによるヌル可能な参照」から「デフォルトによる非ヌル可能な参照」への変更は、ほぼ常に単純な変更であり、タイプシステムが複雑さと戦い、特定のタイプのエラーや意味のない状態を排除する上で非常に優れています。ですから、非常に多くの言語がこのエラーを何度も繰り返し続けているのはかなりクレイジーです。)
他のヒント
オプションタイプの良いところは、それらがオプションであるということではありません。それです 他のすべてのタイプはそうではありません.
時々, 、一種の「ヌル」状態を表現できる必要があります。 「値なし」オプションと、変数が取る可能性のある他の値を表す必要がある場合があります。したがって、フラットアウトする言語は、これが少し不自由になるでしょう。
だが 頻繁, 、私たちはそれを必要としません、そして 許可 このような「ヌル」状態は、あいまいさと混乱のみにつながります:.NETで参照型変数にアクセスするたびに、私はそれを考慮する必要があります それはヌルかもしれません.
多くの場合、それは決してありません 実際に プログラマーはコードが発生しないようにコードを構成するため、ヌルになります。しかし、コンパイラはそれを確認することができず、あなたがそれを見るたびに、あなたは「これはヌルになることはできますか?ここでヌルをチェックする必要がありますか?」と自問する必要があります。
理想的には、ヌルが意味をなさない多くの場合、 許可されるべきではありません.
それは.NETで達成するのが難しいです。そこでは、ほとんどすべてがヌルになる可能性があります。あなたが呼んでいるコードの著者に頼って100%懲戒処分され、一貫性を持っているようにし、nullである可能性とできないものを明確に文書化する必要があります。 すべての.
ただし、タイプがめまいでない場合 デフォルトで, 、そうすれば、それらがnullかどうかを確認する必要はありません。コンパイラ/タイプチェッカーがあなたのためにそれを強制するので、あなたは彼らが決してヌルになることはできないことを知っています。
そして、私たちが私たちがいるまれなケースのためにバックドアが必要です 行う ヌル状態を処理する必要があります。次に、「オプション」タイプを使用できます。次に、「価値なし」のケースを表現できる必要があるという意識的な決定を下した場合にnullを許可します。他のすべてのケースでは、値がnullにならないことがわかります。
他の人が述べたように、たとえばC#またはJavaでは、nullは次の2つのうちの1つを意味します。
- 変数は非初期化されていません。これは、理想的には 一度もない 起こる。変数はそうすべきではありません 存在 初期化されていない限り。
- 変数にはいくつかの「オプションの」データが含まれています。 データーがない. 。これは時々必要です。おそらく、あなたはリスト内のオブジェクトを見つけようとしていますが、それがそこにあるかどうかを事前に知りません。次に、「オブジェクトが見つからなかった」ことを表すことができる必要があります。
2番目の意味は保存する必要がありますが、最初の意味は完全に排除する必要があります。そして、2番目の意味でさえデフォルトであってはなりません。それは私たちがオプトインできるものです 必要な場合. 。しかし、オプションになるものが必要ない場合は、タイプチェッカーが必要です。 保証 それが決してヌルになることはないこと。
これまでのすべての答えは、その理由に焦点を当てています null
悪いことであり、言語が特定の値が保証できる場合、それがちょっと便利です 一度もない nullになります。
その後、彼らはあなたが非無効性を強制するならば、それがかなりきちんとしたアイデアになることを示唆し続けます すべて 値。 Option
また Maybe
常に定義された値があるとは限らないタイプを表すため。これは、Haskellが取ったアプローチです。
それはすべて良いものです!しかし、同じ効果を達成するために明示的にnullable /非ヌルタイプの使用を排除するものではありません。それでは、なぜオプションはまだ良いことなのでしょうか?結局のところ、ScalaはNullable値をサポートしています(is もっている に、それはJavaライブラリで動作することができます)が、サポートします Options
同じように。
Q. それでは、言語からヌルを完全に削除できること以外の利点は何ですか?
A. 構成
null-awareコードから素朴な翻訳を行う場合
def fullNameLength(p:Person) = {
val middleLen =
if (null == p.middleName)
p.middleName.length
else
0
p.firstName.length + middleLen + p.lastName.length
}
オプションを認識するコードへ
def fullNameLength(p:Person) = {
val middleLen = p.middleName match {
case Some(x) => x.length
case _ => 0
}
p.firstName.length + middleLen + p.lastName.length
}
大きな違いはありません!しかし、それはまたです ひどい オプションを使用する方法...このアプローチははるかにクリーンです:
def fullNameLength(p:Person) = {
val middleLen = p.middleName map {_.length} getOrElse 0
p.firstName.length + middleLen + p.lastName.length
}
あるいは:
def fullNameLength(p:Person) =
p.firstName.length +
p.middleName.map{length}.getOrElse(0) +
p.lastName.length
オプションのリストの処理を開始すると、さらに良くなります。リストを想像してみてください people
それ自体がオプションです:
people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)
これはどのように作動しますか?
//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f
//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")
//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe"))
//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe"))
//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)
NULLチェック(またはエルビス:演算子)を使用した対応するコードは、痛みを伴うほど長くなります。ここでの本当のトリックは、フラットマップ操作です。これにより、オプションとコレクションのネストされた理解が、nullable値が達成できない方法で理解できます。
人々はそれを見逃しているようだから: null
あいまいです。
アリスの出生日はです null
. 。どういう意味ですか?
ボブの死日はそうです null
. 。どういう意味ですか?
「合理的な」解釈は、アリスの出産日が存在するが不明であるということかもしれませんが、ボブの死日は存在しません(ボブはまだ生きています)。しかし、なぜ私たちは異なる答えに到達したのですか?
別の問題: null
エッジケースです。
- は
null = null
? - は
nan = nan
? - は
inf = inf
? - は
+0 = -0
? - は
+0/0 = -0/0
?
答えはそうです いつもの 「はい」、「いいえ」、「はい」、「はい」、「いいえ」、「はい」。クレイジーな「数学者」はナンを「nullity」と呼び、それがそれ自体と等しいと言います。 SQLは、ヌルを何にも等しくないものとして扱います(したがって、ナンのように振る舞います)。 ±∞、±0、およびnansを同じデータベース列に保存しようとするとどう思いますか(2があります53 ナン、その半分は「ネガティブ」です)。
さらに悪いことに、データベースはnullの扱い方が異なり、それらのほとんどが一貫していません(参照してください sqliteでのヌルハンドリング 概要については)。それはかなり恐ろしいです。
そして今、義務的な話のために:
最近、5つの列のある(sqlite3)データベーステーブルを設計しました a NOT NULL, b, id_a, id_b NOT NULL, timestamp
. 。これは、かなりarbitrary意的なアプリの一般的な問題を解決するために設計された一般的なスキーマであるため、2つの独自性の制約があります。
UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)
id_a
既存のアプリデザインとの互換性のためにのみ存在します(一部には、より良いソリューションを考えていないため)、新しいアプリでは使用されていません。 nullがSQLで機能する方法のために、挿入できます (1, 2, NULL, 3, t)
と (1, 2, NULL, 4, t)
そして、最初の一意性の制約に違反しないでください(なぜなら (1, 2, NULL) != (1, 2, NULL)
).
これは、ほとんどのデータベースでヌルがどのように機能するかのために特に機能します(おそらく、「実際の」状況をモデル化する方が簡単です。
FWIWは、最初に定義されていない動作を呼び出すことなく、C ++参照は「NULL」を「指す」ことはできません。また、非初期化された参照メンバー変数を使用してクラスを構築することはできません(例外がスローされた場合、建設が失敗します)。
サイドノート:たとえば、相互に独占的なポインター(つまり、それらの1つだけが非ヌルになることができる)が必要な場合があります。 type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed
. 。代わりに、私はのようなことをすることを余儀なくされています assert((bool)actionSheet + (bool)alertView == 1)
.
参照/ポインターを持っていることの望ましくないことは、デフォルトではめまいになります。
これがヌルの主な問題ではないと思います。ヌルの主な問題は、2つのことを意味する可能性があることです。
- 参照/ポインターは無効化されていません。ここでの問題は、一般的に可変性と同じです。 1つは、コードを分析することをより困難にします。
- nullである変数は実際に何かを意味します。これは、オプションタイプが実際に正式化される場合です。
オプションタイプをサポートする言語は、通常、非初期化された変数の使用も禁止または阻止します。
パターンマッチングなどのヌルケースのチェックを容易にする戦略を含むオプションタイプの仕組み。
効果的になるには、オプションタイプを言語で直接サポートする必要があります。それ以外の場合は、それらをシミュレートするために多くのボイラープレートコードが必要です。パターンマッチングとタイプ推論は、オプションタイプを簡単に操作できる2つのキー言語機能です。例えば:
f#:
//first we create the option list, and then filter out all None Option types and
//map all Some Option types to their values. See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]
//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")
ただし、オプションタイプを直接サポートすることなくJavaのような言語では、次のようなものがあります。
//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
if(op instanceof Some)
filteredList.add(((Some<Integer>)op).getValue());
メッセージを食べるメッセージなどの代替ソリューション
Objective-Cの「Message Eating Nil」は、ヌルチェックの頭痛を明るくする試みほど解決策ではありません。基本的に、nullオブジェクトにメソッドを呼び出しようとするときにランタイム例外をスローする代わりに、式は代わりにnull自体を評価します。不信感を停止すると、それはあたかも各インスタンスメソッドが始まるかのようです if (this == null) return null;
. 。しかし、情報の損失があります。メソッドが有効な返品値であるため、またはオブジェクトが実際にnullであるために、メソッドがnullを返したかどうかはわかりません。それは例外を嚥下するようなものであり、以前に概説したNullの問題に対処する進展はありません。
アセンブリは、Untyped Pointersとも呼ばれる住所をもたらしました。 cはタイプされたポインターとして直接マッピングしましたが、すべてのタイプされたポインターと互換性のある一意のポインター値としてアルゴルのヌルを導入しました。 CのNullの大きな問題は、すべてのポインターがnullになる可能性があるため、手動チェックなしではポインターを安全に使用することはできないということです。
高レベルの言語では、nullを持つことは、2つの異なる概念を実際に伝えるため、厄介です。
- 何かを伝えることです 未定義.
- 何かを伝えることです オプション.
未定義の変数を持つことはほとんど役に立たず、それらが発生するたびに未定義の動作にもたらされます。誰もが、定義されていないものをあらゆる犠牲を払って避けるべきであることに誰もが同意すると思います。
2番目のケースはオプションであり、たとえばで明示的に提供されるのが最適です オプションタイプ.
私たちが輸送会社にいるとしましょう。ドライバーのスケジュールを作成するのに役立つアプリケーションを作成する必要があります。ドライバーごとに、次のようないくつかの情報を保存します。緊急時に電話する運転免許証と電話番号。
Cでは、次のことができます。
struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };
struct Driver {
char name[32]; /* Null terminated */
struct PhoneNumber * emergency_phone_number;
struct MotorbikeLicence * motorbike_licence;
struct CarLicence * car_licence;
struct TruckLicence * truck_licence;
};
あなたが観察するように、ドライバーのリストを介した処理では、ヌルポインターを確認する必要があります。コンパイラはあなたを助けません、プログラムの安全はあなたの肩に依存しています。
OCAMLでは、同じコードが次のようになります。
type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }
type driver = {
name: string;
emergency_phone_number: phone_number option;
motorbike_licence: motorbike_licence option;
car_licence: car_licence option;
truck_licence: truck_licence option;
}
それでは、すべてのドライバーの名前をトラックライセンス番号とともに印刷したいとします。
Cで:
#include <stdio.h>
void print_driver_with_truck_licence_number(struct Driver * driver) {
/* Check may be redundant but better be safe than sorry */
if (driver != NULL) {
printf("driver %s has ", driver->name);
if (driver->truck_licence != NULL) {
printf("truck licence %04d-%04d-%08d\n",
driver->truck_licence->area_code
driver->truck_licence->year
driver->truck_licence->num_in_year);
} else {
printf("no truck licence\n");
}
}
}
void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
if (drivers != NULL && nb >= 0) {
int i;
for (i = 0; i < nb; ++i) {
struct Driver * driver = drivers[i];
if (driver) {
print_driver_with_truck_licence_number(driver);
} else {
/* Huh ? We got a null inside the array, meaning it probably got
corrupt somehow, what do we do ? Ignore ? Assert ? */
}
}
} else {
/* Caller provided us with erroneous input, what do we do ?
Ignore ? Assert ? */
}
}
ocamlでそれは次のとおりです。
open Printf
(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
printf "driver %s has " driver.name;
match driver.truck_licence with
| None ->
printf "no truck licence\n"
| Some licence ->
(* Here we are guaranteed to have a licence *)
printf "truck licence %04d-%04d-%08d\n"
licence.area_code
licence.year
licence.num_in_year
(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
List.iter print_driver_with_truck_licence_number drivers
この些細な例でわかるように、安全なバージョンには複雑なものはありません。
- テルサーです。
- はるかに優れた保証が得られ、ヌルチェックはまったく必要ありません。
- コンパイラは、オプションを正しく処理したことを確認しました
Cでは、ヌルチェックとブームを忘れただけかもしれません...
注:これらのコードサンプルはコンパイルされていませんが、アイデアが得られることを願っています。
Microsoft Researchには、インターティングプロジェクトがあります
仕様#
C#拡張子です ヌルタイプではありません そして、いくつかのメカニズム ヌルではないことを防ぎます, 、しかし、私見、を適用します 契約による設計 原則は、ヌルの参照によって引き起こされる多くの厄介な状況により、より適切で役立つかもしれません。
.NETのバックグラウンドから来て、私はいつもNullにはポイントがあると思っていました。私が構造体とそれがどれほど簡単であるかを知るようになるまで、多くのボイラープレートコードを避けました。 トニー・ホアー 2009年のQCon Londonで話し、 ヌルの参照を発明したことを謝罪しました. 。彼を引用する:
私はそれを私の10億ドルの間違いと呼んでいます。それは1965年のヌル参照の発明でした。当時、私はオブジェクト指向の言語(アルゴルW)で参照用の最初の包括的なタイプシステムを設計していました。私の目標は、コンパイラによって自動的に実行されることを確認して、参照のすべての使用が絶対に安全であることを保証することでした。しかし、単に実装がとても簡単だったという理由だけで、ヌルの参照を入れる誘惑に抵抗することはできませんでした。これにより、無数のエラー、脆弱性、システムのクラッシュが発生し、過去40年間に10億ドルの痛みと損傷を引き起こした可能性があります。近年、MicrosoftのプレフィックスやPreastaffastなどの多くのプログラムアナライザーが参照を確認するために使用され、非ヌルである可能性のあるリスクがある場合は警告を発しています。 Spec#のような最近のプログラミング言語は、非ヌル参照の宣言を導入しました。これがソリューションであり、1965年に拒否しました。
この質問も参照してください プログラマーで
Robert Nystromはここで素晴らしい記事を提供しています:
http://journal.stuffwithstuff.com/2010/08/23/void-null-maybe-and-nothing/
不在と失敗のサポートを追加するときの彼の思考プロセスを説明する カササギ プログラミング言語。
私はいつもヌル(またはnil)を見ていました 値がない.
時々あなたはこれを望んでいます、時にはあなたはそうしません。それはあなたが働いているドメインに依存します。不在が意味がある場合:ミドルネームがない場合、アプリケーションはそれに応じて行動できます。一方、ヌル値がそこにないであれば、名はnullである場合、開発者は午前2時の電話を受けます。
また、コードが過負荷になり、nullのチェックが過剰に複雑になっているのを見ました。私にとって、これは2つのことの1つを意味します。
a)アプリケーションツリーのバグアップ
b)悪い/不完全なデザイン
ポジティブな面では、ヌルはおそらく何かがないかどうかを確認するためのより有用な概念の1つであり、nullの概念のない言語は、データ検証を行うときに物事を過度に補完するエンドアップを行います。この場合、新しい変数が初期化されていない場合、言語は通常、変数を空の文字列0または空のコレクションに設定すると述べました。ただし、空の文字列または0または空のコレクションがある場合 有効な値 あなたのアプリケーションのために - あなたは問題を抱えています。
時々、これは、フィールドの特別な/奇妙な値を発明することによって回避され、無知の状態を表すことができます。しかし、その後、意図的なユーザーによって特別な値が入力されたときにどうなりますか?そして、これがデータ検証ルーチンを作る混乱に陥らないようにしましょう。言語がヌルの概念をサポートした場合、すべての懸念は消えます。
ベクトル言語は、nullを持たない場合に逃げることがあります。
空のベクトルは、この場合に型付けされたヌルとして機能します。