Как мы преодолеем время компиляции и время выполнения во время программирования на зависимом языке?

Мне говорят, что в системе зависимого типа "типы" и "значения" смешиваются, и мы можем рассматривать их как "термины".

Но есть кое-что, что я не могу понять: на строго типизированном языке программирования без Dependent Type (например, Haskell) типы определяются (выводятся или проверяются) во время компиляции, но значения определяются (вычисляются или вводятся) во время выполнения.

Я думаю, что между этими двумя этапами должен быть разрыв. Просто подумайте, что если значение интерактивно считывается из STDIN, как мы можем ссылаться на это значение в типе, который нужно решить AOT?

например, существует натуральное число n и список натурального числа xs (который содержит n элементов), который мне нужно прочитать из STDIN, как я могу поместить их в структуру данных Vect n Nat?

Ответ 1

Предположим, что мы вводим n :: Int во время исполнения STDIN. Затем мы читаем n строк и храним их в vn :: Vect n String (делайте вид, что это можно сделать). Аналогично, мы можем читать m :: Int и vm :: Vect m String. Наконец, мы конкатенируем два вектора: vn ++ vm (упрощая здесь бит). Это может быть тип проверен и будет иметь тип Vect (n+m) String.

Теперь верно, что проверка типа выполняется во время компиляции до того n,m как известны значения n,m, а также до vn,vm. Но это не имеет значения: мы все еще можем условно рассуждать о неизвестных n,m и утверждать, что vn ++ vm имеет этот тип, включающий n+m, даже если мы еще не знаем, что такое n+m.

Это не так уж и отличается от математики, где мы манипулируем символическими выражениями с неизвестными переменными в соответствии с некоторыми правилами, даже если мы не знаем значений переменных. Нам не нужно знать, какое число n чтобы видеть, что n+n = 2*n.

Аналогичным образом, тип проверки может ввести проверку

-- 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

Мне кажется, что здесь есть два вопроса:

  1. Учитывая, что некоторые значения неизвестны во время компиляции (например, значения, считанные из STDIN), как мы можем использовать их в типах? (Обратите внимание, что ци уже дал отличный ответ на это.)

  2. Некоторые операции (например, getLine) кажутся совершенно бессмысленными во время компиляции; как мы можем говорить о них по типу?

Ответ на (1), как сказал Чи, является символическим или абстрактным рассуждением. Вы можете прочитать число n, а затем выполнить процедуру, которая создает Vect n Nat путем чтения из командной строки n раз, используя арифметические свойства, такие как факт, что 1+(n-1) = n для ненулевого натуральные числа.

Ответ на (2) немного более тонкий. Наивно, вы можете сказать, что "эта функция возвращает вектор длины n, где n считывается из командной строки". Есть два типа, которые вы можете попробовать дать (извинения, если я получаю Haskell нотации неправильно)

unsafePerformIO (do n <- getLine; return (IO (Vect (read n :: Int) Nat)))

или (в псевдо-Coq-нотации, поскольку я не уверен, что обозначение Haskell для экзистенциальных типов)

IO (exists n, Vect n Nat)

Эти два типа действительно могут быть понятны и сказать разные вещи. Первый тип, для меня, говорит "во время компиляции, считывает n из командной строки и возвращает функцию, которая во время выполнения дает вектор длины n, выполняя IO". Второй тип говорит "во время выполнения, выполнить IO, чтобы получить натуральное число n и вектор длины n ".

Как мне нравится смотреть на это, все побочные эффекты (кроме, возможно, без прерывания) - это монадные трансформаторы, и есть только одна монада: "настоящая" монада. Трансформаторы Monad работают также на уровне типа, как на уровне термина; одна вещь, которая является специальной, run :: M a → a которая исполняет монаду (или стек монадных трансформаторов) в "реальном мире". Есть два момента времени, в которые вы можете вызвать run: один во время компиляции, где вы вызываете любой экземпляр run который отображается на уровне типа. Другая - во время выполнения, где вы вызываете любой экземпляр run который отображается на уровне значений. Обратите внимание, что run имеет смысл только при указании порядка оценки; если ваш язык не определяет, является ли это по умолчанию или по одному по вызову (или по запросу по вызову или по запросу или по вызову-другому), вы можете получить некогерентность, если вы пытаетесь вычислить тип.