Ответ 1
Coq и Agda зависят от типизированных полных языков. Их вдохновляют их относительные теоретико-множественные основы, которые включают (типизированное) лямбда-исчисление, которое (сильно) нормализуется. Это означает, что сокращение любого лямбда-термина в конечном итоге должно прекратиться.
Это свойство позволяет использовать Coq и Agda в качестве доказательной системы: с их помощью можно доказать математические факты. Действительно, по переписке Карри-Говарда, если
someExpression :: someType
то someType
соответствует логической (интуиционистской) тавтологии.
В Haskell это не так, поскольку любой тип может быть "доказан" на
undefined :: someType
то есть. мы можем обмануть, используя "нижнее" значение. Это делает Haskell логичным, непоследовательным. Мы можем доказать undefined :: Data.Void.Void
, что соответствует логическому "ложному" предложению, например. Это плохо, но это необходимая цена за то, что у вас есть неограниченная рекурсия, что позволяет использовать не завершающие программы.
Для сравнения, Coq и Agda имеют только примитивную рекурсию (которая никогда не может повторяться навсегда).
Теперь, к сути. Хорошо известно, что добавление аксиомы * :: *
в Coq/Agda делает логику более непротиворечивой. Мы можем получить доказательство "ложного", используя парадокс Жирара. Это было бы очень плохо, так как мы можем тогда даже доказать такие вещи, как lemma :: Int :~: String
, и получить функцию принуждения coerce :: Int -> String
.
lemma :: Int :~: String
lemma = -- exploit Girard paradox here
-- using Haskell syntax:
coerce :: Int -> String
coerce x = case lemma of Refl -> x
Если бы мы реализовали это наивно, coerce
просто выполнил бы небезопасный листинг, переинтерпретируя базовые биты - в конце концов, это оправдано lemma
, заявив, что эти типы точно такие же! Таким образом, мы потеряем безопасность во время выполнения. Doom ждет.
В Haskell, даже если мы не добавляем * :: *
, мы все равно несовместимы, поэтому мы можем просто иметь
lemma = undefined
и получим coerce
в любом случае! Таким образом, добавление * :: *
на самом деле не увеличивает количество проблем. Это просто еще один источник непоследовательности.
Тогда можно было бы задаться вопросом, почему в Haskell coerce
это безопасный тип. Ну, в Haskell case lemma of Refl ->...
заставляет оценивать lemma
. Это может вызвать только исключение или не завершиться, поэтому часть ...
никогда не будет достигнута. Haskell не может оптимизировать coerce
как небезопасный литье, в отличие от Agda/Coq.
Есть еще один аспект Haskell, который цитируется в параграфе: язык принуждения. Внутренне, когда мы пишем
case lemma1 of
Refl -> case lemma2 of
Refl -> ...
...
Refl -> expression
мы вводим много типов, которые должны быть использованы для доказательства того, что expression
действительно имеет требуемый тип. В Coq программист должен использовать сложную форму соответствия (зависимое сопоставление), чтобы оправдывать, где и как использовать равенства типов. В Haskell компилятор пишет это доказательство для нас (в Coq система типов богаче, и это будет связано с унификацией более высокого порядка, я думаю, что это неразрешимо). Это доказательство НЕ написано в Haskell! В самом деле, Хаскелл непоследовательна, поэтому мы не можем полагаться на это. Вместо этого Haskell использует для этого другой язык принудительного принуждения, который гарантированно будет согласован. Нам нужно только полагаться на это.
Мы можем увидеть некоторые проблески этого внутреннего языка принуждения, если мы сбросим Core. Например, компиляция доказательности транзитивности
trans :: a :~: b -> b :~: c -> a :~: c
trans Refl Refl = Refl
получаем
GADTtransitivity.trans
:: forall a_au9 b_aua c_aub.
a_au9 :~: b_aua -> b_aua :~: c_aub -> a_au9 :~: c_aub
[GblId, Arity=2, Caf=NoCafRefs, Str=DmdType]
GADTtransitivity.trans =
\ (@ a_auB)
(@ b_auC)
(@ c_auD)
(ds_dLB :: a_auB :~: b_auC)
(ds1_dLC :: b_auC :~: c_auD) ->
case ds_dLB of _ [Occ=Dead] { Refl cobox0_auF ->
case ds1_dLC of _ [Occ=Dead] { Refl cobox1_auG ->
(Data.Type.Equality.$WRefl @ * @ a_auB)
`cast` ((<a_auB>_N :~: (Sym cobox0_auF ; Sym cobox1_auG))_R
:: ((a_auB :~: a_auB) :: *) ~R# ((a_auB :~: c_auD) :: *))
}
}
Обратите внимание на cast
в конце, который использует доказательство в языке принуждения
(<a_auB>_N :~: (Sym cobox0_auF ; Sym cobox1_auG))_R
В этом доказательстве мы можем видеть Sym cobox0_auF ; Sym cobox1_auG
, который, как я полагаю, использует симметрию Sym
и транзитивность ;
для достижения желаемой цели: доказательство того, что Refl :: a_auB :~: a_auB
действительно можно безопасно отбросить до желаемого a_auB :~: c_auD
.
Наконец, обратите внимание, что я уверен, что такие доказательства затем отбрасываются во время компиляции GHC и что cast
в конечном итоге сводится к небезопасному приведению во время выполнения (case
все еще оценивает два входных доказательства, чтобы сохранить тип безопасности). Однако наличие промежуточного доказательства гарантирует, что компилятор работает правильно.