web-dev-qa-db-ja.com

nullのない言語の最良の説明

プログラマーがヌルのエラー/例外について不平を言うとき、誰かが私たちがヌルなしで何をするかを尋ねる場合があります。

オプションの種類のクールさに関する基本的な考え方はありますが、それを最もよく表現する知識や言語スキルがありません。 great平均的なプログラマーに親しみやすい方法で書かれた以下の説明は、その人に向けることができますか?

  • 参照/ポインタをデフォルトでnullにすることは望ましくない
  • などのNULLケースのチェックを容易にする戦略を含むオプションタイプの動作方法
    • パターンマッチングと
    • モナド理解
  • ゼロを食べるメッセージなどの代替ソリューション
  • (私が逃した他の側面)
221

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

心配はもうありません。型システムは、class Doorのインスタンスが存在する可能性のある状態が3つのみであることを保証します。これは、型システムが得意とするものです。コンパイル時にエラーのクラス全体を明示的に除外します。

nullの問題は、すべての参照型が、通常は望ましくないスペースでこの余分な状態を取得することです。 string変数は任意の文字シーケンスである可能性がありますが、このクレイジーな余分なnull値が問題ドメインにマップされない可能性もあります。 Triangleオブジェクトには3つのPointがあり、それ自体はXYの値を持っていますが、残念ながらPointsまたはTriangle自体はこれである可能性があります私が作業しているグラフ作成ドメインにとって意味のないクレイジーなヌル値。

存在しない可能性のある値をモデル化する場合は、明示的に選択する必要があります。私が人々をモデル化する方法が、すべてのPersonFirstNameLastNameを持っているが、一部の人だけがMiddleNamesを持っているということなら、

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

ここでstringは、null不可タイプであると想定されています。そうすれば、誰かの名前の長さを計算しようとするときに、確立するのが難しい不変条件や予期しないNullReferenceExceptionsはありません。型システムは、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

心配なし。対照的に、文字列などの型のnull許容参照を持つ言語では、

class Person
    private string FirstName
    private string MiddleName
    private string LastName

あなたは次のようなものを作成することになります

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

入ってくるPersonオブジェクトにすべてが非ヌルの不変式がない場合に爆発します。または

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は最初/最後が存在することを保証しますが、middleはnullである可能性があります。または、異なるタイプの例外をスローするチェックを行うか、誰が何を知っているかもしれません。こうした馬鹿げた実装の選択肢と考えるべきことはすべて、この馬鹿げた表現可能な価値があるからです。

通常、Nullは不必要な複雑さを追加します。複雑さはすべてのソフトウェアの敵であり、妥当な場合はいつでも複雑さを軽減するよう努力する必要があります。

(これらの単純な例でさえ複雑です。FirstNamenullにできない場合でも、string""(空の文字列)を表すことができます。おそらく、nullできない文字列であっても、「意味のない値を表している」という場合があります。再び、不変式と実行時、または型システムを使用した条件付きコード(例:NonEmptyString型を使用)後者はおそらく不適切です(「良い」型は、一般的な操作のセットでしばしば「閉じられる」、例えばNonEmptyString.SubString(0,0)で閉じられていませんが、デザイン空間でより多くのポイントを示しています。最終的に、特定の型システムでは、取得するのに非常に優れた複雑さがあります。このトピックの重要な点は、ほぼevery型システムではchangであるということです。 「デフォルトでヌル可能参照」から「デフォルトでヌル不可能な参照」への変更は、ほとんど常に単純な変更であり、複雑さを解消し、特定のタイプのエラーや無意味な状態を排除する際に型システムを大幅に改善します。したがって、非常に多くの言語がこのエラーを何度も繰り返し続けるのは非常にクレイジーです。)

426
Brian

オプションのタイプの良いところは、オプションであるということではありません。それは、他のすべてのタイプはそうではないということです

Sometimes、一種の「null」状態を表すことができる必要があります。変数が取る可能性のある他の値と同様に、「値なし」オプションを表す必要がある場合があります。したがって、これを完全に禁止する言語は、少し不自由になります。

しかし、多くの場合、必要ありません。allowingこのような「null」状態はあいまいさと混乱のみをもたらします。NETの参照型変数にアクセスするたびに、nullの可能性があると考える必要があります。

多くの場合、プログラマがコードを構造化してコードが決して発生しないようにするため、実際にになることはありません。しかし、コンパイラーはそれを確認できず、それを見るたびに、「これはヌルにならないのか、ここでヌルをチェックする必要があるのか​​?」と自問する必要があります。

理想的には、nullが意味をなさない多くの場合、許可されないです。

ほとんどすべてがヌルになる可能性のある.NETでこれを達成するのは困難です。 100%規律と一貫性を保つために呼び出しているコードの作成者に依存し、nullの可能性と不可能性を明確に文書化するか、妄想してすべてをチェックする必要があります

ただし、型がnull可能でない場合デフォルトの場合、nullであるかどうかを確認する必要はありません。コンパイラ/型チェッカーがそれを強制するので、それらが決してnullになることはありません。

そして、do null状態を処理する必要があるまれな場合のために、バックドアが必要です。次に、「オプション」タイプを使用できます。次に、「値なし」のケースを表現できるようにする必要があるという意識的な決定を行った場合はnullを許可し、他のすべてのケースでは値がnullにならないことを知っています。

他の人が言及したように、たとえばC#またはJavaでは、nullは次の2つのいずれかを意味します。

  1. 変数は初期化されていません。これは、理想的には、never発生しないはずです。初期化されない限り、変数はexistであってはなりません。
  2. 変数にはいくつかの「オプション」データが含まれています。データがないの場合を表すことができる必要があります。これは時々必要です。おそらく、リスト内のオブジェクトを見つけようとしているのに、それがそこにあるかどうかを事前に知りません。次に、「オブジェクトが見つかりませんでした」ことを表すことができる必要があります。

2番目の意味は保持する必要がありますが、最初の意味は完全に排除する必要があります。そして、2番目の意味でさえデフォルトではありません。これは、必要なときに必要な場合にオプトインできるものです。しかし、オプションにする必要がない場合は、型チェッカーにguaranteeを指定して、決してnullにならないようにします。

63
jalf

これまでのすべての答えは、なぜnullが悪いのか、そして特定の値がnever nullであることを言語が保証できる場合にそれがどのように便利であるかに焦点を当てています。

その後、all値に対してnull不可を強制する場合、かなりきちんとしたアイデアになることを提案します。常に定義された値を持つとは限らない型を表すOptionMaybeのような概念。これはHaskellが取ったアプローチです。

それはすべて良いものです!しかし、同じ効果を達成するために明示的にnull可能/ null以外の型を使用することを妨げません。それでは、なぜOptionがまだ良いのでしょうか?結局、Scalaはnull値をサポートします(hasであるため、Javaライブラリで動作します)ただし、Optionsもサポートしています。

Q。それでは、言語からヌルを完全に削除できることを超える利点は何ですか?

A。構成

Null対応のコードから単純な翻訳を行う場合

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

オプションのリストの処理を開始すると、さらに良くなります。 List 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チェック(またはelvis?:演算子)を含む対応するコードは非常に長くなります。ここでの本当のトリックは、null許容値が決して達成できない方法でオプションとコレクションのネストされた理解を可能にするflatMap操作です。

44
Kevin Wright

人々はそれを見逃しているように見えるので:nullはあいまいです。

アリスの生年月日はnullです。どういう意味ですか?

ボブの死亡日はnullです。どういう意味ですか?

「合理的な」解釈は、アリスの生年月日は存在するが不明であるのに対し、ボブの生年月日は存在しない(ボブはまだ生きている)ということです。しかし、なぜ私たちは別の答えに到達したのでしょうか?


別の問題:nullはエッジの場合です。

  • null = nullですか?
  • nan = nanですか?
  • inf = infですか?
  • +0 = -0ですか?
  • +0/0 = -0/0ですか?

答えは、通常は「yes」、「no」、「yes」、「yes」、「no」、「yes」です。クレイジーな「数学者」はNaNを「無効」と呼び、それを自分自身と同等だと言います。 SQLは、nullを何とも等しくないものとして扱います(したがって、それらはNaNのように動作します)。 ±∞、±0、およびNaNを同じデータベース列に保存しようとするとどうなりますか(2つあります)53 NaN、その半分は「負」です)。

さらに悪いことに、データベースはNULLの扱い方が異なり、それらのほとんどは一貫性がありません(概要については SQLiteでのNULL処理 を参照してください)。それはかなり恐ろしいです。


そして今、義務的な話のために:

最近、5つの列a NOT NULL, b, id_a, id_b NOT NULL, timestampを持つ(sqlite3)データベーステーブルを設計しました。かなり任意のアプリの一般的な問題を解決するために設計された汎用スキーマであるため、2つの一意性制約があります。

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_aは、既存のアプリ設計との互換性のためにのみ存在し(部分的にはより良い解決策を考え出していないため)、新しいアプリでは使用されません。 SQLでのNULLの動作方法により、(1, 2, NULL, 3, t)および(1, 2, NULL, 4, t)を挿入でき、最初の一意性制約に違反しません((1, 2, NULL) != (1, 2, NULL)のため)。

これは特に、ほとんどのデータベースの一意性制約でNULLが機能するために機能します(おそらく、実際の状況をモデル化する方が簡単です。たとえば、2人が同じ社会保障番号を持つことはできませんが、すべての人が持っているわけではありません)。


FWIW、未定義の動作を最初に呼び出さないと、C++参照はnullを「ポイント」できず、初期化されていない参照メンバー変数を使用してクラスを構築することはできません(例外がスローされた場合、構築は失敗します)。

補足:時々、相互に排他的なポインターが必要になる場合があります(つまり、そのうちの1つだけがNULL以外になることがあります)。仮想のiOS type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissedで。代わりに、assert((bool)actionSheet + (bool)alertView == 1)のようなことを強制されます。

38
tc.

デフォルトで参照/ポインタを持つことは望ましくない

これがnullの主な問題だとは思わない。nullの主な問題は、nullが2つのことを意味する可能性があることだ。

  1. 参照/ポインターは初期化されていません:ここでの問題は、一般的な可変性と同じです。 1つは、コードの分析がより困難になることです。
  2. 変数がnullであるということは、実際には何かを意味します。これは、Option型が実際に形式化する場合です。

オプションタイプをサポートする言語は、通常、初期化されていない変数の使用も禁止または阻止します。

パターンマッチングなどのNULLケースのチェックを容易にする戦略を含むオプションタイプの動作方法

有効にするには、言語でオプションタイプを直接サポートする必要があります。それ以外の場合は、それらをシミュレートするために多くの定型コードが必要です。パターンマッチングと型推論は、Option型を簡単に操作できるようにする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;")

ただし、Option型を直接サポートしていない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());

nilを食べるメッセージなどの代替ソリューション

Objective-Cの「nilを食べるメッセージ」は、nullチェックの頭痛を軽減する試みほどの解決策ではありません。基本的に、nullオブジェクトのメソッドを呼び出そうとしたときにランタイム例外をスローする代わりに、式は代わりにnullに評価されます。信じられないことを中断すると、各インスタンスメソッドがif (this == null) return null;で始まるようになります。ただし、情報が失われます。有効な戻り値であるため、またはオブジェクトが実際にnullであるために、メソッドがnullを返したかどうかはわかりません。これは例外の嚥下によく似ており、以前に概説したnullの問題に対処するための進展はありません。

16
Stephen Swensen

アセンブリは、型指定されていないポインターとしても知られるアドレスをもたらしました。 Cはそれらを型付きポインターとして直接マップしましたが、すべての型付きポインターと互換性のある一意のポインター値としてALGOLのnullを導入しました。 Cでのnullの大きな問題は、すべてのポインターがnullになる可能性があるため、手動チェックなしでは安全にポインターを使用できないことです。

高レベル言語では、nullを使用すると2つの異なる概念が実際に伝わるため、扱いにくいです。

  • 何かがundefinedであることを伝える。
  • 何かがオプションであることを伝える。

未定義の変数を持つことはほとんど役に立たず、それらが発生するたびに未定義の動作になります。私は、未定義のものを持っていることはどんな犠牲を払っても避けるべきであることに誰もが同意すると思います。

2番目のケースはオプションであり、たとえば option type を使用して明示的に提供するのが最適です。


運送会社にいて、ドライバーのスケジュールを作成するのに役立つアプリケーションを作成する必要があるとします。ドライバーごとに、所有している運転免許証や緊急時に電話する電話番号など、いくつかの情報を保存します。

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;
};

ご覧のとおり、ドライバーのリストに対する処理では、nullポインターをチェックする必要があります。コンパイラはあなたを助けません。プログラムの安全性はあなたの肩にかかっています。

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

この些細な例でわかるように、安全なバージョンでは複雑なことは何もありません:

  • 簡単です。
  • あなたははるかに良い保証を得て、nullチェックは全く必要ありません。
  • コンパイラーは、オプションを正しく処理したことを確認しました

一方、Cでは、nullチェックとブームを忘れていたかもしれません...

注:これらのコードサンプルはコンパイルされていませんが、アイデアが得られれば幸いです。

11
bltxd

Microsoft Researchには、

スペック#

これは、not-nullタイプと、オブジェクトがnullでないことをチェックするメカニズムを持つC#拡張です。 design by contract原則を適用することは、null参照によって引き起こされる多くの厄介な状況に対してより適切でより役立つ場合があります。

5
Jahan Zinedine

.NETのバックグラウンドから来た私は、nullにはポイントがあるといつも思っていました。構造体について知り、構造体を使って簡単に作業できることを知り、多くの定型コードを回避するまで。 Tony Hoare 2009年のQCon Londonでの講演、 null参照の発明に謝罪 。彼を引用するには:

私はそれを10億ドルの間違いと呼んでいます。それは1965年のnull参照の発明でした。当時、私はオブジェクト指向言語(ALGOL W)での参照のための最初の包括的な型システムを設計していました。私の目標は、コンパイラによって自動的に実行されるチェックを使用して、参照のすべての使用が絶対に安全であることを保証することでした。しかし、実装が非常に簡単だったという理由だけで、null参照を挿入する誘惑に抵抗することはできませんでした。これにより、無数のエラー、脆弱性、およびシステムクラッシュが発生し、過去40年間で数十億ドルの痛みと損害をもたらした可能性があります。近年、MicrosoftのPREfixやPREfastなどの多くのプログラムアナライザーが参照を確認するために使用され、それらがnullでない可能性がある場合は警告を発します。 Spec#のような最近のプログラミング言語では、非ヌル参照の宣言が導入されています。これが解決策であり、1965年に拒否しました。

この質問も参照してください プログラマー

4
nawfal

ロバート・ナイストロムはここで素晴らしい記事を提供しています:

http://journal.stuffwithstuff.com/2010/08/23/void-null-maybe-and-nothing/

Magpie プログラミング言語に不在と失敗のサポートを追加するときの彼の思考プロセスを説明します。

3
Corbin March

私は常にNull(またはnil)を値の欠如であると考えてきました。

これが必要な場合とそうでない場合があります。作業しているドメインによって異なります。不在が意味がある場合:ミドルネームがない場合、アプリケーションはそれに応じて動作できます。一方、null値が存在するべきではない場合:名がnullの場合、開発者は2時の電話をかけます。

また、nullのチェックでコードがオーバーロードされ、複雑になっているのを見ました。私にとって、これは次の2つのいずれかを意味します。
a)アプリケーションツリーの上位のバグ
b)不良/不完全な設計

プラス面-ヌルはおそらく何かが存在しないかどうかをチェックするためのより有用な概念の1つであり、ヌルの概念を持たない言語は、データ検証を行うときに物事を過度に複雑にします。この場合、新しい変数が初期化されていないと、前述の言語は通常、変数を空の文字列、0、または空のコレクションに設定します。ただし、アプリケーションで空の文字列または0または空のコレクションが有効な値である場合は、問題があります。

これは、初期化されていない状態を表すフィールドに特別な/奇妙な値を発明することで回避される場合があります。しかし、善意のユーザーが特別な値を入力するとどうなりますか?また、これによりデータ検証ルーチンが混乱することはありません。言語がヌルの概念をサポートしていれば、すべての懸念は消えます。

1
Jon

ベクトル言語は、nullを持たないことで回避できる場合があります。

この場合、空のベクトルは型付きnullとして機能します。

0
Joshua