1. ホーム
  2. programming-languages

[解決済み】NULLのない言語についての最適な説明

2022-04-05 09:05:22

質問

プログラマがNULLエラーや例外について文句を言っていると、しばしば誰かがNULLなしでどうするのかと聞いてきます。

オプション型のカッコよさはなんとなくわかっているのですが、それをうまく表現する知識も言語スキルもないのです。とは何ですか? すごい について、一般のプログラマが親しみやすいように書かれた説明で、その人に向けて発信できるようなものを教えてください。

  • 参照やポインタがデフォルトでnullになることの好ましくない点
  • オプションタイプはどのように機能するか?
    • パターンマッチと
    • 単項式内包
  • nilを食べるメッセージなどの代替案
  • (その他、見落としていた点)

解決方法は?

なぜnullが望ましくないかを簡潔にまとめると 無意味な状態は表現可能であってはならない .

例えば、ドアをモデル化するとしよう。 開いている状態、閉まっているが鍵がかかっていない状態、閉まっているが鍵がかかっている状態の3つのうち1つを取ることができる。 この場合、次のようにモデル化することができます。

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 を持ち、それ自体が XY の値ですが、残念ながら Point または Triangle は、私が取り組んでいるグラフの領域では無意味な、このクレイジーなnull値であるかもしれません。 などなど。

存在しないかもしれない値をモデル化するつもりなら、それを明示的に選択すべきです。 もし私が人をモデル化する方法が、すべての Person には FirstNameLastName が、一部の人しか持っていない。 MiddleName のようなことを言いたいのです。

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

ここで string は null でない型であると仮定します。 そうすれば、トリッキーな不変量が設定されることはなく、予期せぬ NullReferenceException は、誰かの名前の長さを計算しようとしたときに発生します。 型システムによって 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

これは、入力される Person オブジェクトがすべて非 null という不変条件を持っていない場合に吹き飛んでしまいます。

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 あるいは、異なるタイプの例外を投げるようなチェックを行うか、あるいは誰にもわからないことです。 このようなおかしな実装の選択や考えなければならないことが出てくるのは、この馬鹿げたrepresentable-valueがあるからで、それはあなたが望んでいない、あるいは必要ないものです。

Nullは通常、不必要な複雑さを追加します。 複雑さはすべてのソフトウェアの敵であり、合理的であればいつでも複雑さを減らすように努力すべきです。

(このような単純な例でさえも、さらに複雑であることに注意してください。 たとえ FirstName にすることはできません。 null , a string を表すことができます。 "" (空文字列)であり、これもおそらく我々がモデル化しようと思っている人名ではないでしょう。 このように、Nullにならない文字列であっても、意味のない値を表現している可能性があるのです。 この場合も、実行時の不変条件や条件コードで対処するか、あるいは型システム(たとえば NonEmptyString という型があります)。 後者はあまりお勧めできません(quot;good" 型はしばしば共通の操作のセットに対して "closed" ですし、例えば NonEmptyString に対して閉じているわけではありません。 .SubString(0,0) ) が、設計空間におけるより多くのポイントを示している。 結局のところ、どのような型システムであっても、取り除くのに非常に適した複雑さと、本質的に取り除くのが難しい複雑さがあるのです。 このトピックで重要なのは、ほとんどの場合 あらゆる 型システムにおいて、quot;nullable references by default" から "non-nullable references by default" への変更は、ほぼ常に、型システムの複雑さを解消し、ある種のエラーや無意味な状態を排除する上で非常に優れたものにする単純な変更なのだそうです。 だから、多くの言語がこのエラーを何度も何度も繰り返しているのは、かなり異常なことなのだ)。