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


49

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

関数の戻り値の型がボトム型の場合、それは戻り値がないことを意味します。期間。永久にループするか、例外をスローする可能性があります。

プログラミング言語でこの奇妙なタイプを持つことのポイントは何ですか?それほど一般的ではありませんが、ScalaやLispなどの一部に存在します。


2
@SargeBorsch:確かですか?もちろん1はCで明示的に定義することはできませんvoid...データを
バジーレStarynkevitch

3
@BasileStarynkevitchにはtypeの値はなく、ユニットタイプにはvoid1つの値が必要です。また、指摘したように、typeの値を宣言することさえできませんvoid。つまり、それは型ではなく、言語の特別な特殊なケースです。
セージボルシュ

2
はい、Cはこの点で奇妙です。特に、ポインターおよび関数ポインター型の記述方法は奇妙です。しかしvoid、Java でもほぼ同じです。実際には型ではなく、値を持つことはできません。
セージボルシュ

3
ボトム型の言語のセマンティクスでは、ボトム型は値を持たないと見なされますが、1つの値(ボトム値)を持ち、決して完了しない(通常)計算を表します。ボトムの値はすべてのタイプの値であるため、ボトムのタイプはすべてのタイプのサブタイプになります。
セオドアノーベル

4
@BasileStarynkevitch Common Lispには、値を持たないnil型があります。また、1つの値、シンボル(別名、)のみを持つnullタイプがあり、これはユニットタイプです。nil()
ジョシュアテイラー

回答:


33

簡単な例を取り上げます:C ++ vs 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 ++コンストラクトは、戻り値を指定しますが、戻り値を返さないことも指定します。それは少し奇妙です。

標準的な注意事項では、C ++構文はC ++ 11でのみ登場しました(トップに追加されました)が、さまざまなコンパイラがしばらくの間さまざまな拡張機能を提供していたため、サードパーティの分析ツールをプログラミングしてさまざまな方法を認識する必要がありましたこの属性は記述できます。それを標準化することは明らかに明らかに優れています。


さて、利益は?

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

  • 最適化:後のコードを削除することができます(返されません)。レジスタを保存する必要はありません(復元する必要がないため)。
  • 静的分析:多数の潜在的な実行パスを排除します
  • 保守性:(静的分析を参照、ただし人間による)

6
voidC ++の例では、戻り値の型ではなく、関数の型(の一部)を定義しています。関数が許可される値を制限しますreturn。voidに変換できるもの(これは何もありません)。関数returnsの後に値を続けてはなりません。関数の完全なタイプはvoid () (char const*, char const*, int, char const *)です。+ 1使用するchar const代わりにconst char:-)
鮮明

4
ただし、ボトム型を使用する方が意味があるわけではなく、関数が言語の一部として戻るかどうかについて注釈を付ける方が理にかなっています。実際には、関数はさまざまな理由で復帰に失敗する可能性があるため、副作用に基づいて関数に注釈を付けるという比較的最近の概念のように、キャッチオール用語を使用するのではなく、何らかの方法で理由をエンコードする方が良いようです。
グレッグロス

2
実際には、「戻らない」と「戻りタイプX」を独立させる理由があります。呼び出し規約は戻りタイプに依存する可能性があるため、独自のコードの下位互換性があります。
デデュプリケーター

ある[[noreturn]] 構文や機能の追加のパー?
ザイビス

1
[続き]全体として、Iの利点に関する議論では、whatの実装として何が適格であるかを定義する必要があります。そして、(a →⊥)≤(ab)を持たない型システムは、⊥の便​​利な実装だとは思いません。そのため、この意味で、SysV x86-64 C ABI(その他)は、implementingの実装を許可していません。
アレックスシュピルキン

26

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

if E then A else B

タイプのすべての値含むタイプであるべきAとのタイプのすべての値をB。タイプがいる場合BNothing、その後のタイプ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(Karlの答えで述べたように)別のユースケースは、List [Nothing]です。リストのタイプは、各メンバーのタイプがNothingです。したがって、空のリストのタイプになります。

Nothingこれらのユースケースを機能させる重要な特性は、値がないことではありません(たとえば、Scalaでは値がありません)。これは、他のすべてのタイプのサブタイプであるということです。

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

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

if E then A else error ""

および式の型は、型と同じになりA、任意の発現のために、A

Haskellの空のリストには型スキームがありforall a. [a]ます。場合はA、型がリスト型がある表現はその後、あります

if E then A else []

は、と同じ型の式Aです。


forall a . [a][a]Haskellの型の違いは何ですか?型変数は、Haskell型式ですでに一般的に定量化されていませんか?
ジョルジオ

@Giorgio Haskellでは、型スキームを見ていることが明らかな場合、普遍的な数量化は暗黙的です。forall標準のHaskell 2010 で書くことさえできません。これはHaskellフォーラムではなく、Haskellの規約に慣れていない人もいるので、明示的に記述しました。そのforall a . [a]ため、標準ではないのを除いて違いはありません[a]
セオドアノーベル

19

型は2つの方法でモノイドを形成し、一緒に半環を作ります。それが代数的データ型と呼ばれるものです。有限型の場合、この半環は自然数の半環(ゼロを含む)に直接関係します。つまり、型が持つ可能性のある値の数をカウントします(「終了しない値」を除く)。

  • 一番下のタイプ(これを呼び出しますVacuous)の値はゼロです
  • 単位タイプには1つの値があります。型とその単一の値の両方を呼び出します()
  • 構成(ほとんどのプログラミング言語は、パブリックフィールドを持つレコード/構造体/クラスを通じて、非常に直接サポートしています)は製品操作です。例えば、(Bool, Bool)すなわち、4つの可能な値を有し(False,False)(False,True)(True,False)および(True,True)
    ユニットタイプは、構成操作のアイデンティティ要素です。たとえば((), False)、と((), True)はtypeの唯一の値である((), Bool)ため、このタイプはBoolそれ自体と同型です。
  • 代替型は、ほとんどの言語でいくらか無視されます(OO言語は、継承によってそれらをサポートします)が、それほど有用ではありません。2つのタイプの間の代替にはAB基本的にすべての値がありA、プラスのすべての値がありB、したがって合計タイプです。たとえば、Either () Bool3つの値があり、それらをLeft ()Right Falseおよびと呼びますRight True
    ボトム型は、和のアイデンティティの要素である:Either Vacuous A持っているだけで、フォームの値をRight a、のでLeft ...意味がありません(Vacuous値がありません)。

これらのモノイドについて興味深いのは、言語に関数を導入すると、関数を射とするこれらのタイプのカテゴリモノイドのカテゴリになることです。とりわけ、これにより、アプリカティブファンクターとモナドを定義することができます。これらは、それ以外の場合は純粋に機能的な用語での一般的な計算(おそらく副作用などを含む)の優れた抽象化になります。

さて、実際には、問題の片側だけを心配することでかなり遠くまで到達できます(コンポジションモノイド)。たとえば、Haskellでさえ長い間、標準のボトムタイプを持っていませんでした。今では、と呼ばれていVoidます。

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

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


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


「一番下の型(これを呼び出しますVoid)」。これは、Haskellの任意の型のメンバーであるvalue と混同しないでください。bottom
mb21

18

永久にループするか、例外をスローする可能性があります。

これらの状況で役立つタイプのように聞こえますが、まれではあります。

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


12
「Haskellの空のリストは型コンストラクターです」:確かにここで関連するのは、ポリモーフィックまたはオーバーロードです。つまり、異なる型の空のリストは個別の値ですが[]、それらすべてを表し、インスタンス化されます必要に応じて特定のタイプ。
ピータールファヌラムスデイン

興味深いことに、Haskellインタープリターで空の配列を作成しようとすると、非常に不明確な型の非常に明確な値が得られます[a]。同様に、:t Left 1yields Num a => Either a b。実際には表現力のタイプを評価しaますが、ないのbEither Integer b
ジョン・ドヴォルザーク

5
空のリストはコンストラクタです。少し紛らわしいことに、関係する型コンストラクターは同じ名前ですが、空のリスト自体は型ではなく値です(まあ、型レベルのリストもありますが、それはまったく別のトピックです)。空のリストを任意のリストタイプで機能させる部分は、forallそのタイプに含まれていforall a. [a]ます。を考えるにはいくつかの良い方法がありますがforall、実際に理解するには少し時間がかかります。
デビッド

@PeterLeFanuLumsdaineそれがまさに型コンストラクターという意味です。それは、種類が以外の型であることを意味し*ます。
グレッグロス

2
Haskell []では、タイプコンストラクターであり[]、空のリストを表す式です。しかし、それは「Haskellの空リストが型コンストラクタであること」を意味するものではありません。コンテキストは[]、型として使用されているか式として使用されているかを明確にします。宣言するとしますdata Foo x = Foo | Bar x (Foo x); Foo型コンストラクタまたは値として使用できるようになりましたが、偶然両方に同じ名前を選択したのは偶然です。
セオドアノーベル

3

特定のコードパスに到達できないという事実を文書化することは、静的分析に役立ちます。たとえば、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非戻りとしてマークされた場合、コンパイラは警告する必要はありません。


2

一部の言語でnullは、すべてのタイプのサブタイプがどの言語にnullを使用するかをうまく定義しているため、ボトムタイプがあります(nullそれ自体とそれ自体を返す関数の両方であるという穏やかな矛盾にもかかわらず、bot無人である必要がある理由に関する一般的な議論を回避します)。

また、機能タイプ(any -> bot)のキャッチオールとして使用して、異常終了したディスパッチを処理することもできます。

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


11
いいえ、ボトムタイプはユニットタイプではありません。ボトムタイプには値がまったくないため、ボトムタイプを返す関数は戻りません(つまり、例外またはループを無期限にスローします)
Basile Starynkevitch

@BasileStarynkevitch-私はユニットの種類について話していません。ユニットタイプvoidは、共通言語でマッピングされます(ただし、同じ使用に対してわずかに異なるセマンティクスが使用されます)null。また、ほとんどの言語では、nullをボトムタイプとしてモデル化していないことも正しいです。
テラスティン

3
@TheodoreNorvell- Tangentの初期バージョンはそれを行いました-私はそれが著者であるので、それはおそらく不正行為です。他の人のためにリンクを保存していませんが、その調査をしてからしばらく経ちました。
Telastyn

1
@Martijnしかしnull、使用することができます、例えばnull、ブール値の結果を得るためにポインターを比較します。答えは、2つの異なる種類のボトムタイプがあることを示していると思います。(a)言語(Scalaなど)で、すべてのタイプのサブタイプであるタイプが、結果を提供しない計算を表す場合。本質的には空のタイプですが、技術的には、非終了を表す役に立たないボトム値が多く含まれています。(b)タンジェントなどの言語。ボトムタイプは、他のすべてのタイプにもある有用な値を含むため、他のすべてのタイプのサブセットです。null。
セオドアノーベル

4
いくつかの言語には宣言できない型の値があり(nullリテラルに共通)、他の言語には宣言できるが値がない型(従来のボトム型)があり、それらはやや同等の役割を果たしている。
マーティン

1

はい、これは非常に便利なタイプです。その役割は主に型システムの内部にありますが、ボトム型が公然と表示される場合があります。

条件が式である静的に型付けされた言語を考えてみましょう(if-then-else構造はCとその友人の三項演算子としても機能し、同様の多方向のcase文があるかもしれません)。関数型プログラミング言語にはこれがありますが、特定の命令型言語でも同様に発生します(ALGOL 60以降)。その後、すべての分岐式は最終的に条件式全体の型を生成する必要があります。単純にそれらの型を等しくすることを要求できます(そして、これはCの三項演算子の場合だと思います)。一般に、各分岐式は(暗黙的に)変換可能であることが望まれます。 完全な式の型となる一般的な型に(おそらく、その一般的な型をコンパイラで効果的に検出できるようにするための多少複雑な制限がありますが、C ++を参照してください。ただし、ここでは詳しく説明しません)。

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

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


それでは、式が他に何も得られないので、as var foo = someCondition() ? functionReturningBar() : functionThatAlwaysThrows()のタイプを推測できるという考えはありますか?fooBar
スーパーキャット

1
少なくとも回答の最初の部分で、ユニットタイプを説明しました。ユニットの種類を返す関数を返す関数として宣言されているものと同じであるvoidC.にあなたは決して戻り、または全くelements-とリスト関数のタイプについて話あなたの答えの第二部、ある確かにボトムタイプ!(多くの場合、と_|_いうよりもむしろ書かれてい*ます。理由は
わかり

2
疑念を避けるために、「有用なものを返さない」ことは「返さない」とは異なります。最初は、ユニットタイプで表されます。2番目はボトムタイプです。
アンドリュー

@andrewf:はい、私はその区別を理解しています。私の答えは少し長めですが、私がしたかった点は、特定の表現をより柔軟に(しかしまだ安全に)使用できるようにするために、ユニットタイプとボトムタイプの両方が(異なるが)同等の役割を果たしているということです。
マークヴァンレーベン

@supercat:はい、それがアイデアです。現在ならば、それは有効であるだろうが、違法であるC ++でfunctionThatAlwaysThrows()明示的に置き換えられた throwため、標準で特別な言語に、。これを行うタイプを持つことは改善になります。
マークヴァン

0

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

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.