誰かが依存型を私に説明できますか?Haskell、Cayenne、Epigram、またはその他の関数型言語の経験はほとんどないので、使用できる用語が単純であればあるほど、感謝します。
誰かが依存型を私に説明できますか?Haskell、Cayenne、Epigram、またはその他の関数型言語の経験はほとんどないので、使用できる用語が単純であればあるほど、感謝します。
回答:
これを考慮してください:すべてのまともなプログラミング言語で、あなたは関数を書くことができます、例えば
def f(arg) = result
ここでf
は、値arg
を取り、値を計算しますresult
。値から値への関数です。
現在、一部の言語では、ポリモーフィック(別名ジェネリック)値を定義できます。
def empty<T> = new List<T>()
ここでempty
は、型T
を取り、値を計算します。型から値への関数です。
通常、ジェネリック型の定義もあります。
type Matrix<T> = List<List<T>>
この定義は型を取り、型を返します。タイプからタイプへの関数として見ることができます。
通常の言語が提供するものについてはこれだけです。言語が4番目の可能性、つまり値から型への関数の定義も提供する場合、その言語は依存型と呼ばれます。つまり、値に対する型定義のパラメーター化:
type BoundedInt(n) = {i:Int | i<=n}
いくつかの主流の言語は、混同されるべきではないこれのいくつかの偽の形式を持っています。たとえば、C ++では、テンプレートは値をパラメーターとして受け取ることができますが、適用する場合はコンパイル時定数である必要があります。本当に依存型の言語ではそうではありません。たとえば、上記のタイプを次のように使用できます。
def min(i : Int, j : Int) : BoundedInt(j) =
if i < j then i else j
ここで、関数の結果タイプは実際の引数値に依存するj
ため、用語が異なります。
BoundedInt
例は実際には詳細化タイプではありませんか?これは「かなり近い」ですが、たとえばIdrisがdep.typingに関するチュートリアルで最初に言及している「依存型」の種類とは異なります。
依存型を使用すると、コンパイル時に、より多くの論理エラーを排除できます。これを説明するために、関数に関する次の仕様を検討してf
ください。
関数
f
は、入力として偶数の整数のみを取る必要があります。
依存型がないと、次のようなことができます。
def f(n: Integer) := {
if n mod 2 != 0 then
throw RuntimeException
else
// do something with n
}
ここで、コンパイラーn
は、実際に偶数であるかどうかを検出できません。つまり、コンパイラーの観点からは、次の式で問題ありません。
f(1) // compiles OK despite being a logic error!
このプログラムは実行され、実行時に例外をスローします。つまり、プログラムに論理エラーがあります。
現在、依存型を使用すると、表現力が大幅に向上し、次のような記述が可能になります。
def f(n: {n: Integer | n mod 2 == 0}) := {
// do something with n
}
これn
は依存型{n: Integer | n mod 2 == 0}
です。これを大声で読み上げると役立つかもしれません
n
は、各整数が2で割り切れるような整数のセットのメンバーです。
この場合、コンパイラはコンパイル時に奇数を渡した論理エラーを検出しf
、プログラムが最初に実行されないようにします。
f(1) // compiler error
これは、このような要件を満たす関数の実装を試みる方法のScalaパス依存型を使用した実例f
です。
case class Integer(v: Int) {
object IsEven { require(v % 2 == 0) }
object IsOdd { require(v % 2 != 0) }
}
def f(n: Integer)(implicit proof: n.IsEven.type) = {
// do something with n safe in the knowledge it is even
}
val `42` = Integer(42)
implicit val proof42IsEven = `42`.IsEven
val `1` = Integer(1)
implicit val proof1IsOdd = `1`.IsOdd
f(`42`) // OK
f(`1`) // compile-time error
キーは、値がどのように通知にあるn
値の種類に表示されます。proof
すなわちn.IsEven.type
:
def f(n: Integer)(implicit proof: n.IsEven.type)
^ ^
| |
value value
タイプ n.IsEven.type
は値に 依存すると言うn
ので、依存型という用語があります。
f(random())
、コンパイルエラーが発生しますか?
f
ある式に適用するには、コンパイラが(あなたの助けの有無にかかわらず)式が常に偶数であることを提供する必要があり、そのような証明は存在しないためrandom()
(実際には奇数である可能性があるため)、f(random())
コンパイルに失敗します。
C ++を知っている場合は、やる気を起こさせる例を簡単に提供できます。
いくつかのコンテナタイプとその2つのインスタンスがあるとしましょう
typedef std::map<int,int> IIMap;
IIMap foo;
IIMap bar;
そして、このコードフラグメントを検討してください(fooは空ではないと思われるかもしれません):
IIMap::iterator i = foo.begin();
bar.erase(i);
これは明らかなゴミです(そしておそらくデータ構造を破壊します)が、「iteratorintofoo」と「iteratorintobar」はIIMap::iterator
意味的に完全に互換性がないにもかかわらず、同じタイプであるため、タイプチェックは問題なく行われます。
問題は、イテレータタイプがコンテナタイプだけでなく、実際にはコンテナオブジェクトに依存する必要があることです。つまり、「非静的メンバータイプ」である必要があります。
foo.iterator i = foo.begin();
bar.erase(i); // ERROR: bar.iterator argument expected
このような機能、つまり用語(foo)に依存する型(foo.iterator)を表現する機能は、まさに依存型の意味です。
この機能があまり見られない理由は、ワームの大きな缶が開かれるためです。コンパイル時に2つのタイプが同じかどうかを確認するために、2つの式を証明する必要がある状況に突然陥ります。同等です(実行時に常に同じ値を生成します)。あなたはウィキペディアの比較すればその結果、依存型付け言語のリストにそのと定理証明器のリストを、あなたは不審な類似性に気づくことがあります。;-)
本のタイプとプログラミング言語(30.5)の引用:
この本の多くは、さまざまな種類の抽象化メカニズムの形式化に関係しています。単純型付きラムダ計算では、項を取得してサブ項を抽象化する操作を形式化し、後で別の項に適用することでインスタンス化できる関数を生成しました。システム
F
では、用語を取り出して型を抽象化し、さまざまな型に適用することでインスタンス化できる用語を生成する操作を検討しました。にλω
、単純に型付きラムダ計算のメカニズムを「1レベル上」に要約し、型を取得して部分式を抽象化し、後でさまざまな型に適用することでインスタンス化できる型演算子を取得しました。これらすべての形式の抽象化を考える便利な方法は、他の式によって索引付けされた式のファミリーの観点からです。通常のラムダ抽象化λx:T1.t2
は、terms[x -> s]t1
によってインデックス付けされた用語のファミリーですs
。同様に、型の抽象化λX::K1.t2
は型 によって索引付けされた用語のファミリーであり、型演算子は型によって索引付けされた型のファミリーです。
λx:T1.t2
用語で索引付けされた用語のファミリー
λX::K1.t2
タイプによって索引付けされた用語のファミリー
λX::K1.T2
タイプによってインデックス付けされたタイプのファミリーこのリストを見ると、まだ検討していない可能性が1つあることは明らかです。それは、用語で索引付けされた型族です。この形式の抽象化も、依存型のルーブリックの下で広く研究されてきました。