Как понимать общий тип "леммы"?

Возможно, это глупый вопрос. Здесь цитата из документ хазотизма:

Один из подходов к решению этой проблемы - кодирование лемм, параметризованных уравнений, как функции Хаскелла. В общем, такие леммы могут быть закодированы как функции типа:

∀ x1 ... xn. Natty x1 → ... → Natty xn → ((l ~ r) ⇒ t) → t

Я думал, что понял RankNTypes, но я не могу понять последнюю часть этого предложения. Я читаю его неофициально как "заданный термин, который требует l ~ r, вернуть этот термин". Я уверен, что это толкование неверно, потому что это, по-видимому, приводит к округлости: мы не знаем l ~ r до завершения самого доказательства, поэтому как можно ожидать, что мы будем рассматривать как предположение о доказательстве того термина, который требует, чтобы?

Я бы ожидал, что доказательство равенства будет иметь тип, подобный этому:

Natty x1 → ... → Natty xn → l :~: r

Неформально, "учитывая связку Natty s, верните доказательство того, что предложения l и r равны" (с использованием GHC Data.Type.Equality). Это имеет для меня гораздо больше смысла и, похоже, соответствует тому, что вы скажете в других системах с навязчивой системой. Я предполагаю, что это эквивалентно версии в газете, но я изо всех сил пытаюсь скомпоновать две версии.

Короче, я в замешательстве. Я чувствую, что мне не хватает ключевого понимания. Как читать тип ((l ~ r) => t) -> t?

Ответ 1

Я читаю его как "заданный термин, который требует l ~ r, верните это Термин"

Он "задает термин, тип которого содержит l, возвращает этот термин, при котором все l заменяются на r в типе" (или в другом направлении r -> l). Это очень аккуратный трюк, который позволяет вам делегировать все cong, trans, subst и подобные вещи в GHC.

Вот пример:

{-# LANGUAGE GADTs, DataKinds, PolyKinds, TypeFamilies, TypeOperators, RankNTypes #-}

data Nat = Z | S Nat

data Natty n where
    Zy :: Natty Z
    Sy :: Natty n -> Natty (S n)

data Vec a n where
    Nil  :: Vec a Z
    Cons :: a -> Vec a n -> Vec a (S n)

type family (n :: Nat) :+ (m :: Nat) :: Nat where
    Z     :+ m = m
    (S n) :+ m = S (n :+ m)

assoc :: Natty n -> Natty m -> Natty p -> (((n :+ m) :+ p) ~ (n :+ (m :+ p)) => t) -> t
assoc  Zy     my py t = t
assoc (Sy ny) my py t = assoc ny my py t

coerce :: Natty n -> Natty m -> Natty p -> Vec a ((n :+ m) :+ p) -> Vec a (n :+ (m :+ p))
coerce ny my py xs = assoc ny my py xs

UPDATE

Поучительно специализироваться assoc:

assoc' :: Natty n -> Natty m -> Natty p ->
            (((n :+ m) :+ p) ~ (n :+ (m :+ p)) => Vec a (n :+ (m :+ p)))
                                               -> Vec a (n :+ (m :+ p))
assoc'  Zy     my py t = t
assoc' (Sy ny) my py t = assoc ny my py t

coerce' :: Natty n -> Natty m -> Natty p -> Vec a ((n :+ m) :+ p) -> Vec a (n :+ (m :+ p))
coerce' ny my py xs = assoc' ny my py xs

Даниэль Вагнер объяснил, что происходит в комментариях:

Или, скажем так, вы можете прочитать ((l ~ r) = > t) → t как "заданное термин, который хорошо набран, предполагая, что l ~ r, возвращают тот же самый член из контекста, где мы доказали l ~ r и разрядили это предположение".

Давайте уточним доказательную часть.

В случае assoc' Zy my py t = t n равен Zy и, следовательно,

((Zy :+ m) :+ p) ~ (Zy :+ (m :+ p))

которая сводится к

(m :+ p) ~ (m :+ p)

Это ясно тождество и, следовательно, мы можем выполнить это предположение и вернуть t.

На каждом рекурсивном шаге мы поддерживаем

((n :+ m) :+ p) ~ (n :+ (m :+ p))

уравнение. Поэтому, когда assoc' (Sy ny) my py t = assoc ny my py t уравнение становится

((Sy n :+ m) :+ p) ~ (Sy n :+ (m :+ p))

которая сводится к

 Sy ((n :+ m) :+ p) ~ Sy (n :+ (m :+ p))

из-за определения (:+). А так как конструкторы являются инъективными

constructors_are_injective :: S n ~ S m => Vec a n -> Vec a m
constructors_are_injective xs = xs

уравнение становится

((n :+ m) :+ p) ~ (n :+ (m :+ p))

и мы можем вызывать assoc' рекурсивно.

Наконец, в вызове coerce' эти два члена унифицированы:

 1. ((n :+ m) :+ p) ~ (n :+ (m :+ p)) => Vec a (n :+ (m :+ p))
 2.                                      Vec a ((n :+ m) :+ p)

Ясно, что Vec a ((n :+ m) :+ p) есть Vec a (n :+ (m :+ p)) в предположении, что ((n :+ m) :+ p) ~ (n :+ (m :+ p)).

Ответ 2

Я бы ожидал, что доказательство равенства будет иметь тип, подобный этому:

Natty x1 → ... → Natty xn → l :~: r

Это разумная альтернатива. Фактически, это логически эквивалентно таковому в газете Ха- хоцизма:

{-# LANGUAGE GADTs, RankNTypes, TypeOperators, ScopedTypeVariables #-}
module Hasochism where

data l :~: r where
  Refl :: l :~: l

type Hasoc l r = forall t. (l ~ r => t) -> t

lemma1 :: forall l r. Hasoc l r -> l :~: r
lemma1 h = h Refl 

lemma2 :: forall l r. l :~: r -> Hasoc l r
lemma2 Refl t = t

В некотором смысле Hasoc l r - это непроизводительное кодирование ограничения l ~ r.

Хазотистский вариант несколько проще в использовании, чем :~: один, в том случае, когда вы, например,

type family A a
-- ...
proof1 :: Proxy a -> Hasoc a (A a)
proof1 _ = -- ...

вы можете просто использовать его, как в

use1 :: forall a. [a] -> [A a]
use1 t = proof1 (Proxy :: Proxy a) t

Вместо этого с

proof2 :: Proxy a -> a :~: A a
proof2 _ = -- ...

вам понадобится

use2 :: forall a. [a] -> [A a]
use2 t = case proof2 (Proxy :: Proxy a) of Refl -> t

Ответ 3

У нас были отличные ответы, но, как преступник, я подумал, что предлагаю некоторые замечания.

Да, есть несколько эквивалентных представлений этих лемм. Презентация, которую я использую, является одной из них, и выбор во многом прагматичный. В эти дни (в более поздней кодовой базе) я зашел так далеко, чтобы определить

-- Holds :: Constraint -> *
type Holds c = forall t . (c => t) -> t

Это пример типа выталкивателя: он абстрагирует то, что он поставляет (мотив устранения), и требует, чтобы вы построили ноль или несколько методов (один здесь) для достижения мотивов при более конкретных обстоятельствах. Способ прочитать это назад. В нем говорится:

Если у вас есть проблема (чтобы обитать любой тип мотива t), и никто другой не может помочь, возможно, вы можете добиться прогресса, предположив ограничение c в своем методе.

Учитывая, что язык ограничений допускает конъюнкцию (aka tupling), мы приобретаем средства для записи лемм вида

lemma :: forall x1 .. xn. (p1[x1 .. xn],.. pm[x1 .. xn])        -- premises
                       => t1[x1 .. xn] -> .. tl[x1 .. xn]       -- targets
                       -> Holds (c1[x1 .. xn],.. ck[x1 .. xn])  -- conclusions

и даже может быть, что некоторое ограничение, предпосылка p или заключение c, имеет вид уравнения

l[x1 .. xn] ~ r[x1 .. cn]

Теперь, чтобы развернуть такой lemma, рассмотрим проблему заполнения отверстия

_ :: Problem

Уточните этот _ путем устранения lemma, указав цели. Мотив исходит из проблемы. Метод (единственный в случае Holds) остается открытым.

lemma target1 .. targetl $ _

и отверстие метода не изменит тип

_ :: Problem

но GHC будет знать кучу больше материала и, следовательно, с большей вероятностью верят вашему решению.

Иногда существует выбор ограничений и данных для того, чтобы сделать предположение (ограничение) и то, что (цель данных). Я предпочитаю выбирать их, чтобы избежать двусмысленности (Саймону нравится угадывать x1 .. xn, но иногда ему нужен намек) и облегчить доказательство по индукции, что намного проще для целей (часто для синглетов для данных уровня), чем в помещениях.

Что касается развертывания, для уравнений вы можете, конечно, переключиться на представление типа данных и развернуть анализ случая

case dataLemma target1 .. targetl of Refl -> method

и действительно, если вы экипируете себя Dict экзистенциальным

data Dict (c :: Constraint) :: * where
  Dict :: c => Dict c

вы можете сделать кучу сразу

case multiLemma blah blah blah of (Refl, Dict, Dict, Refl) -> method

но форма выталкивателя более компактна и читаема, когда существует не более одного метода. Действительно, мы можем связать несколько лемм без скольжения вправо

lemma1 ..   $
...
lemmaj ..   $
method

Если у вас есть такой выпрямитель с двумя или более случаями, я думаю, что часто лучше его обертывать как GADT, так что сайты использования могут помечать каждый случай меткой конструктора.

Во всяком случае, да, нужно выбрать представление фактов, которые наиболее компактно позволяют нам расширить охват механизмов управления ограничениями GHC, чтобы больше вещей было просто typechecks. Если вы находитесь в ломе с Саймоном, это часто хорошая стратегия, чтобы объяснить себя Димитрию по соседству.