web-dev-qa-db-ja.com

依存型付き言語でプログラミングする場合、コンパイル時間と実行時のギャップをどのように克服しますか?

依存型システムでは、「型」と「値」が混在していて、どちらも「項」として扱うことができると聞いています。

しかし、理解できないことがあり、依存型のない強く型付けされたプログラミング言語(Haskellなど)では、型はコンパイル時で決定(インファーまたはチェック)されますが、値は決定されます(計算または入力)実行時

これら二つの段階の間にギャップがあるに違いないと思います。値がSTDINからインタラクティブに読み取られる場合、AOTで決定する必要がある型でこの値をどのように参照できると思いますか?

例えば自然数nと自然数xs(n個の要素を含む)のリストがあり、STDINから読み取る必要があります。これらをデータ構造に入れるにはVect n Nat

35
luochen1990

実行時に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)
...
_

「タイプチェッカーは、実行時にfstaがどの型になるかを知らずにbをどのようにチェックできるのでしょうか?」再び、象徴的に推論することによって。

型引数の場合、これは間違いなくより明白です。なぜなら、消去できない上記の_n :: Int_のような値パラメーターとは異なり、通常は型消去後にプログラムを実行するからです。それでも、型を超えて、またはIntを超えて普遍的に定量化することには、いくつかの類似点があります。

43
chi

ここには2つの質問があるようです。

  1. コンパイル時に不明な値(STDINから読み取られた値など)がある場合、それらを型でどのように使用できますか? ( chiはすでにこれに対して優れた答えを出していることに注意してください 。)

  2. 一部の操作(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は、評価順序を指定した場合にのみ意味があることに注意してください。値による呼び出しか名前による呼び出し(または呼び出しによる値か、値による呼び出しか、必要な呼び出しか何かによる呼び出し)が言語で指定されていない場合、次の場合に矛盾が発生する可能性があります。タイプを計算しようとします。

3
Jason Gross