依存型システムでは、「型」と「値」が混在していて、どちらも「項」として扱うことができると聞いています。
しかし、理解できないことがあり、依存型のない強く型付けされたプログラミング言語(Haskellなど)では、型はコンパイル時で決定(インファーまたはチェック)されますが、値は決定されます(計算または入力)実行時。
これら二つの段階の間にギャップがあるに違いないと思います。値がSTDINからインタラクティブに読み取られる場合、AOTで決定する必要がある型でこの値をどのように参照できると思いますか?
例えば自然数n
と自然数xs
(n個の要素を含む)のリストがあり、STDINから読み取る必要があります。これらをデータ構造に入れるにはVect n Nat
?
実行時にSTDINから_n :: Int
_を入力するとします。次に、n
文字列を読み取り、それらを_vn :: Vect n String
_に格納します(これが可能な場合は、今のふりをしてください)。同様に、_m :: Int
_および_vm :: Vect m String
_を読み取ることができます。最後に、2つのベクトルを連結します:_vn ++ vm
_(ここで少し簡略化)。これは型チェックすることができ、型はVect (n+m) String
になります。
これで、型チェッカーは、値_n,m
_がわかる前、および_vn,vm
_がわかる前に、コンパイル時に実行されることは事実です。しかし、これは問題ではありません。未知数_n,m
_に対して記号的にを推論し、_vn ++ vm
_が_n+m
_を含むその型を持っていると主張することはできません。まだ_n+m
_が実際に何であるかを知っています。
変数の値がわからなくても、未知の変数を含むシンボリック式をいくつかのルールに従って操作する数学の場合とそれほど変わりません。 _n+n = 2*n
_を表示するために、n
が何であるかを知る必要はありません。
同様に、型チェッカーはcheckと入力できます
_-- pseudocode
readNStrings :: (n :: Int) -> IO (Vect n String)
readNStrings O = return Vect.empty
readNStrings (S p) = do
s <- getLine
vp <- readNStrings p
return (Vect.cons s vp)
_
(まあ、依存関係のマッチングと再帰が含まれるため、実際には、プログラマーからの型チェックにはさらにいくつかの助けが必要になるかもしれません。しかし、これは無視します。)
重要なことに、型チェッカーはn
が何であるかを知らなくてもそれをチェックできます。
同じ問題が実際に多態性関数ですでに発生していることに注意してください。
_fst :: forall a b. (a, b) -> a
fst (x, y) = x
test1 = fst @ Int @ Float (2, 3.5)
test2 = fst @ String @ Bool ("hi!", True)
...
_
「タイプチェッカーは、実行時にfst
とa
がどの型になるかを知らずにb
をどのようにチェックできるのでしょうか?」再び、象徴的に推論することによって。
型引数の場合、これは間違いなくより明白です。なぜなら、消去できない上記の_n :: Int
_のような値パラメーターとは異なり、通常は型消去後にプログラムを実行するからです。それでも、型を超えて、またはInt
を超えて普遍的に定量化することには、いくつかの類似点があります。
ここには2つの質問があるようです。
コンパイル時に不明な値(STDINから読み取られた値など)がある場合、それらを型でどのように使用できますか? ( chiはすでにこれに対して優れた答えを出していることに注意してください 。)
一部の操作(getLine
など)は、コンパイル時にまったく意味がないように見えます。どのようにしてタイプについてそれらについて話すことができるでしょうか?
(1)への答えは、カイが言ったように、象徴的または抽象的な推論です。数値n
を読み取ってから、コマンドラインからn
回読み取ることで_Vect n Nat
_を構築するプロシージャを作成し、次のような演算プロパティを利用できます。 1+(n-1) = n
非ゼロの自然数。
(2)に対する答えはもう少し微妙です。単純に、「この関数は長さn
のベクトルを返します。ここでn
はコマンドラインから読み取られます」と言うこともできます。あなたがこれを与えようとするかもしれない2つのタイプがあります(私がHaskellの表記が間違っている場合は謝罪)
_unsafePerformIO (do n <- getLine; return (IO (Vect (read n :: Int) Nat)))
_
または(存在型のHaskellの表記が何であるかわからないため、疑似Coq表記で)
_IO (exists n, Vect n Nat)
_
これらの2つのタイプは、実際には両方とも意味があり、異なることを言うことができます。最初のタイプは、「コンパイル時にコマンドラインからn
を読み取り、実行時にIOを実行することによって長さn
のベクトルを与える関数を返す」と私には言います。 2番目のタイプは、「実行時に、IOを実行して自然数n
と長さn
のベクトルを取得する」を示します。
私がこれを見るのが好きなのは、すべての副作用(おそらく非終端以外)はモナド変換子であり、モナドは「実世界」モナドが1つしかないということです。モナド変換子は、用語レベルと同様にタイプレベルでも機能します。特別なものの1つは、モナド(またはモナド変換子のスタック)を「実世界」で実行する_run :: M a -> a
_です。 run
を呼び出すことができる時点は2つあります。1つはコンパイル時に、型レベルで表示されるrun
のインスタンスを呼び出すことです。もう1つは実行時であり、値レベルで表示されるrun
のインスタンスを呼び出します。 run
は、評価順序を指定した場合にのみ意味があることに注意してください。値による呼び出しか名前による呼び出し(または呼び出しによる値か、値による呼び出しか、必要な呼び出しか何かによる呼び出し)が言語で指定されていない場合、次の場合に矛盾が発生する可能性があります。タイプを計算しようとします。