web-dev-qa-db-ja.com

プログラミング言語にボトムタイプがある理由はありますか?

ボトムタイプは、主に数学のタイプ理論に現れる構成です。空の型とも呼ばれます。これは値を持たないタイプですが、すべてのタイプのサブタイプです。

関数の戻り値の型が一番下の型の場合、それは戻りません。限目。多分それは永遠にループするか、多分それは例外をスローします。

この奇妙な型をプログラミング言語で持つ意味は何ですか?それはそれほど一般的ではありませんが、ScalaやLISPなど)には存在します。

50
GregRos

簡単な例を挙げましょう:C++対Rust。

C++ 11で例外をスローするために使用される関数を次に示します。

[[noreturn]] void ThrowException(char const* message,
                                 char const* file,
                                 int line,
                                 char const* function);

そして、これはRustの同等のものです:

fn formatted_panic(message: &str, file: &str, line: isize, function: &str) -> !;

純粋に構文上の問題では、Rust構成の方が賢明です。C++構成ではreturn typeが指定されていることに注意してください。それは少し変です。

標準的なメモでは、C++構文はC++ 11でのみ表示されました(上に追加されました)が、さまざまなコンパイラがしばらくの間さまざまな拡張機能を提供していたため、サードパーティの分析ツールはさまざまな方法を認識するようにプログラムする必要がありましたこの属性は書き込むことができます。それを標準化することは明らかに優れています。


さて、メリットは?

関数が返らないという事実は、次の場合に役立ちます。

  • 最適化:その後コードをプルーニングでき(戻りません)、レジスタを保存する必要はありません(レジスタを復元する必要がないため)、...
  • 静的分析:多くの潜在的な実行パスを排除します
  • 保守性:(静的分析を参照、ただし人間による)
33
Matthieu M.

カールの答えは良いです。ここに私が他の誰も言及していないと思う追加の使用があります。のタイプ

_if E then A else B
_

Aの型のすべての値とBの型のすべての値を含む型である必要があります。 BのタイプがNothingの場合、if式のタイプはAのタイプにすることができます。ルーチンを宣言します

_def unreachable( s:String ) : Nothing = throw new AssertionError("Unreachable "+s) 
_

コードに到達することは期待されていないと言うこと。そのタイプはNothingであるため、unreachable(s)は、結果のタイプに影響を与えることなく、ifまたは(より頻繁に)switchで使用できるようになりました。例えば

_ val colour : Colour := switch state of
         BLACK_TO_MOVE: BLACK
         WHITE_TO_MOVE: WHITE
         default: unreachable("Bad state")
_

ScalaにはそのようなNothingタイプがあります。

Nothingのもう1つの使用例は、(カールの回答で述べたように)List [Nothing]で、メンバーのタイプがNothingであるリストのタイプです。したがって、それは空のリストのタイプになります。

これらのユースケースを機能させるNothingの重要なプロパティはnotです。これは、値がないことです-たとえば、Scalaでは、値がない-それはそれです。他のすべてのタイプのサブタイプです。

すべての型に同じ値が含まれている言語があるとしましょう-それを_()_と呼びましょう。そのような言語では、_()_を唯一の値として持つユニットタイプは、すべてのタイプのサブタイプになる可能性があります。これは、OPが意味する意味で、これをボトムタイプにするものではありません。 OPは、ボトムタイプに値が含まれていないことを明確にしました。ただし、すべてのタイプのサブタイプであるタイプであるため、ボトムタイプとほぼ同じ役割を果たすことができます。

Haskellは少し異なる方法で物事を行います。 Haskellでは、決して値を生成しない式は、型スキーム_forall a.a_を持つことができます。このタイプスキームのインスタンスは他のタイプと統合されるため、(標準の)Haskellにサブタイプの概念がない場合でも、実質的にボトムタイプとして機能します。たとえば、標準のプレリュードのerror関数には、タイプスキーム_forall a. [Char] -> a_があります。だからあなたは書くことができます

_if E then A else error ""
_

また、式のタイプは、任意の式AAのタイプと同じになります。

Haskellの空のリストには、型スキーム_forall a. [a]_があります。 Aがタイプがリストタイプの式である場合、

_if E then A else []
_

Aと同じ型の式です。

27

タイプは2つの方法で monoid を形成し、一緒に semiring を作成します。それが 代数的データ型 と呼ばれるものです。有限タイプの場合、この半リングは自然数(ゼロを含む)の半リングに直接関係します。つまり、タイプが持つ可能な値の数を数えます(「非終了値」を除く)。

  • 一番下の型(Vacuousと呼びます)の値は0です
  • ユニットタイプには1つの値があります。タイプとその単一の値の両方を()と呼びます。
  • 構成(ほとんどのプログラミング言語が、レコード/構造体/パブリックフィールドを持つクラスを介して直接サポートする)は、product操作です。たとえば、(Bool, Bool)には、(False,False)(False,True)(True,False)(True,True)の4つの値があります。
    ユニットタイプは、合成操作のID要素です。例えば。 ((), False)((), True)((), Bool)型の唯一の値であるため、この型はBool自体と同型です。
  • ほとんどの言語では代替型は多少無視されています(オブジェクト指向言語は継承で一種のサポートを提供します)が、それほど有用ではありません。 2つのタイプABの間の代替には、基本的にAのすべての値とBのすべての値があるため、合計タイプ。たとえば、Either () Boolには3つの値があります。これらをLeft ()Right FalseRight Trueと呼びます。
    一番下の型は、合計のID要素です。Either Vacuous Aは意味がないため、Right aには、Left ...の形式のonly値があります(Vacuousには値がありません)。

これらのモノイドについて興味深いのは、あなたの言語にfunctionsを導入すると、これらの型の category が射として機能するということです モノイドカテゴリ 。とりわけ、これにより、アプリケーションファンクタと monads を定義できます。これは、他の純粋に機能的な用語内での一般的な計算(おそらく副作用などを含む)の優れた抽象化であることがわかります。

さて、実際には問題の片側(コンポジションモノイド)のみを心配することでかなり遠くまで行けるので、実際にはボトムタイプを明示的に必要としません。たとえば、Haskellでも長い間、標準的なボトムタイプはありませんでした。現在は Void と呼ばれています。

しかし、全体像を bicartesian closed category と考えると、型システムは実際にはラムダ計算全体と同等であるため、基本的にはチューリング完全言語で可能なすべてのことを完全に抽象化できます。 。埋め込みドメイン固有の言語に最適です。たとえば、 電子回路をこのように直接コーディングするプロジェクト があります。

もちろん、これはすべての理論家の 一般的なナンセンス であると言うこともできます。良いプログラマーになるためにカテゴリー理論について知る必要はまったくありませんが、そうすることで、コードについて推論し、不変条件を証明する強力で途方もなく一般的な方法が得られます。


mb21は、これをbottom値と混同しないでください。 Haskellのような怠惰な言語では、every型にはで示される最下位の「値」が含まれます。これは、明示的に渡すことができる具体的なものではなく、たとえば、関数が永久にループするときに「返される」ものです。 HaskellのVoidタイプでさえ、最下位の値、つまり名前を「含んでいます」。その観点から、Haskellのボトムタイプには実際に1つの値があり、そのユニットタイプには2つの値がありますが、カテゴリ理論の議論では、これは通常無視されます。

19
leftaroundabout

多分それは永遠にループするか、多分それは例外をスローします。

それらの状況で役立つタイプのように聞こえますが、まれかもしれません。

また、Nothing(ボトムタイプのScalaの名前)には値を指定できませんが、List[Nothing]にはその制限がないため、空のリストのタイプとして便利です。ほとんどの言語は、文字列の空のリストを整数の空のリストとは異なる型にすることでこれを回避します。これは一種の理にかなっていますが、空のリストを書くのにより冗長になり、これはリスト指向言語の大きな欠点です。

18
Karl Bielefeldt

静的分析では、特定のコードパスに到達できないという事実を文書化すると便利です。たとえば、C#で次のように記述したとします。

int F(int arg) {
 if (arg != 0)
  return arg + 1; //some computation
 else
  Assert(false); //this throws but the compiler does not know that
}
void Assert(bool cond) { if (!cond) throw ...; }

コンパイラは、Fが少なくとも1つのコードパスで何も返さないことを報告します。 Assertが非戻りとしてマークされる場合、コンパイラーは警告する必要はありません。

3
usr

一部の言語では、nullが最下位の型を持っています。すべての型のサブタイプがnullを使用する言語を適切に定義しているためです(nullがそれ自体とそれ自体を返す関数の両方であるという穏やかな矛盾にもかかわらず) 、botを無人にする理由に関する一般的な議論を避けます)。

また、関数型(any -> bot)失敗したディスパッチを処理します。

一部の言語では、実際にbotをエラーとして解決できます。これを使用して、カスタムコンパイラエラーを提供できます。

2
Telastyn

一部の言語では、関数に注釈を付けて、コンパイラと開発者の両方に、この関数の呼び出しが返されないことを伝えることができます(関数が返せるように関数が記述されている場合、コンパイラはそれを許可しません)。これは知っておくと便利なことですが、最終的には、他のようにこのような関数を呼び出すことができます。コンパイラーはこの情報を使用して、最適化、デッドコードに関する警告などを行うことができます。したがって、このタイプを使用する非常に説得力のある理由はありませんが、それを回避する非常に説得力のある理由もありません。

多くの言語では、関数は「void」を返すことができます。それが正確に何を意味するかは言語に依存します。 Cでは、関数は何も返しません。 Swiftでは、それは関数が可能な値を1つだけ持つオブジェクトを返すことを意味します。可能な値は1つだけなので、その値はゼロビットを取り、実際にはコードを必要としません。どちらの場合も、「下」とは異なります。

「bottom」は可能な値のないタイプです。それは決して存在することはできません。関数が「bottom」を返す場合、返すことができる「bottom」型の値がないため、実際に返すことはできません。

言語設計者がそのように感じた場合、そのタイプを持たない理由はありません。実装は難しくありません(voidを返し、「返されない」とマークされている関数とまったく同じように実装できます)。同じ型ではないため、bottomを返す関数へのポインターとvoidを返す関数へのポインターを混在させることはできません。

1
gnasher729

はい、これは非常に便利なタイプです。その役割はほとんど型システムの内部にありますが、下部の型が公然と現れる場合があります。

条件式が式である静的に型付けされた言語を考えてください(したがって、if-then-else構文はCおよびその仲間の 項演算子 を兼ねており、同様の多元格ステートメントがある場合があります)。関数型プログラミング言語にはこれがありますが、特定の命令型言語でも発生します(ALGOL 60以降)。次に、すべての分岐式は最終的に条件式全体の型を生成する必要があります。単純にそれらの型が等しいことを要求することもできますが(これはCの3項演算子の場合だと思います)、これは特に条件文を条件文としても使用できる場合(有用な値を返さない場合)に過度に制限されます。一般に、各分岐式を(暗黙的に)convertibleにして、完全な式の型になる共通の型にしたい(おそらく多かれ少なかれ共通の型をコンパイラが効果的に見つけられるようにするための複雑な制限(C++を参照)。ただし、ここではそれらの詳細については触れません。

一般的な種類の変換によって、このような条件式の必要な柔軟性が可能になる状況には2種類あります。 1つはすでに説明されており、結果のタイプはユニットタイプvoidです。これは当然、他のすべてのタイプのスーパータイプであり、任意のタイプを(自明に)変換できるようにすることで、条件式を条件ステートメントとして使用できるようになります。もう1つは、式が有用な値を返すが、1つ以上のブランチが値を生成できない場合に関係します。これらは通常、例外を発生させるかジャンプを伴い、式全体の型の値を(到達不能なポイントから)生成するように(また)要求することは無意味です。例外を発生させる句、ジャンプ、および呼び出しをそのような効果を持つようにすることで、この種の状況を適切に処理できます。一番下のタイプは、他のタイプに(自明に)変換できる1つのタイプです。

任意の型への変換可能性を示すために、*のような最下部の型を書くことをお勧めします。それは内部的に他の有用な目的に役立つかもしれません。例えば、何も宣言しない再帰関数の結果型を推測しようとするとき、型推論者は型*を再帰呼び出しに割り当ててチキンを回避できます。卵の状況;実際のタイプは非再帰的ブランチによって決定され、再帰的ブランチは非再帰的ブランチの一般的なタイプに変換されます。 no非再帰的分岐がまったくある場合、型は*のままで、関数に可能な方法がないことを正しく示します再帰から戻ること。これ以外に、例外スロー関数の結果タイプとして、空のリストなど、長さ0のシーケンスのコンポーネントタイプとして*を使用できます。型が[*](必ずしも空のリスト)の式から要素が選択された場合、結果の型*は、これがエラーなしで戻ることができないことを正しく示します。

1