Що таке коротке, але повне пояснення системи чистого / залежного типу?


32

Якщо щось просто, то це повинно бути повністю зрозумілим за допомогою кількох слів. Це можна зробити для λ-обчислення:

Λ-обчислення - це синтаксична граматика (в основному, структура) з правилом скорочення (що означає, що процедура пошуку / заміни повторно застосовується до кожного виникнення конкретного шаблону, поки такого шаблону не існує).

Граматика:

Term = (Term Term) | (λ Var . Term) | Var

Правило скорочення:

((λ var body) term) -> SUBS(body,var,term)
    where `SUBS` replaces all occurrences of `var`
    by `term` in `body`, avoiding name capture.

Приклади:

(λ a . a)                             -> (λ a a)
((λ a . (λ b . (b a))) (λ x . x))     -> (λ b . (b (λ x x)))
((λ a . (a a)) (λ x . x))             -> (λ x . x)
((λ a . (λ b . ((b a) a))) (λ x . x)) -> (λ b . ((b (λ x . x)) (λ x . x)))
((λ x . (x x)) (λ x . (x x)))         -> never halts

Хоча дещо неформально, можна стверджувати, що це достатньо інформативно, щоб нормальна людина зрозуміла λ-числення в цілому - і для цього потрібно 22 рядки відмітки. Я намагаюся зрозуміти системи чистого / залежного типу , що використовуються Idris / Agda та подібними проектами, але більш коротке пояснення, яке я міг знайти, було просто Easy - чудовий папір, але, здається, передбачає багато попередніх знань (Haskell, індуктивний визначення), яких у мене немає. Я думаю, що щось більш стисне, менш багате могло б усунути деякі з цих бар'єрів. Таким чином,

Чи можна дати коротке, повне пояснення систем чистого / залежного типу, у тому ж форматі я представив λ-числення вище?


4
Правила систем чистого типу дуже короткі. Просто Easy - це реалізація залежних типів.

2
Отже, це не "вороже" у сенсі образи, але в тому сенсі, як ви думаєте, я вимагаю багато, щоб не проявити достатньо зусиль у пошуку відповіді власноруч? Якщо це так, я погоджуюсь, що це питання може зажадати дуже багато, тому, можливо, це просто погано. Але за цим стоїть також багато зусиль, ти вважаєш, я повинен редагувати свої спроби?
MaiaVictor

3
Я теж ображаюся від імені моїх співавторів, які написали текст "Навчального втілення залежно набраного обчислення лямбди", який замінив "Просто легко" як робочу назву. Я написав ядро ​​коду, яке є машинкою в <100 рядків Haskell.

2
Тоді я, звичайно, погано висловився. Я люблю папір "Просто легко" і читаю її на кожному перерві з декількох днів тому - це єдине, що в світі дало мені часткове відчуття, я починаю розуміти тему (і повірте, я намагався) . Але я думаю, що він спрямований на громадськість з більшою кількістю знань, ніж у мене, і тому я все ще маю труднощів отримати частину цього. Нічого не пов’язаного з якістю паперу, але мої власні обмеження.
MaiaVictor

1
@pigworker і код - це моя улюблена частина цього, саме тому, що він (стосовно англійського пояснення) - набагато коротше, але все-таки повне пояснення цілого, як я тут запитав. Чи трапляється у вас копія коду, який я можу завантажити?
MaiaVictor

Відповіді:


7

Відмова від відповідальності

Це дуже неформально, як ви просили.

Граматика

У мові, що залежить від тиску, у нас є сполучна на рівні типу, а також на рівні значень:

Term = * | (∀ (Var : Term). Term) | (Term Term) | (λ Var. Term) | Var

Добре набраний термін - це термін із доданим типом, ми напишемо t ∈ σабо

σ
t

щоб вказати, що термін tмає тип σ.

Правила введення тексту

Для простоти ми вимагаємо, щоб в λ v. t ∈ ∀ (v : σ). τобох λі прив’язували ту саму змінну ( vв даному випадку).

Правила:

t ∈ σ is well-formed if σ ∈ * and t is in normal form (0)

*            ∈ *                                                 (1)
∀ (v : σ). τ ∈ *             -: σ ∈ *, τ ∈ *                     (2)
λ v. t       ∈ ∀ (v : σ). τ  -: t ∈ τ                            (3)
f x          ∈ SUBS(τ, v, x) -: f ∈ ∀ (v : σ). τ, x ∈ σ          (4)
v            ∈ σ             -: v was introduced by ∀ (v : σ). τ (5)

Таким чином, *є "тип усіх типів" (1), утворює типи з типів (2), лямбда-абстракції мають pi-типи (3), а якщо vвводиться ∀ (v : σ). τ, то vмає тип σ(5).

"у звичайній формі" означає, що ми виконуємо якомога більше скорочень за допомогою правила скорочення:

Правило скорочення

(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ) ~> SUBS(b, v, t) ∈ SUBS(τ, v, t)
    where `SUBS` replaces all occurrences of `v`
    by `t` in `τ` and `b`, avoiding name capture.

Або в двовимірному синтаксисі де

σ
t

означає t ∈ σ:

(∀ (v : σ). τ) σ    SUBS(τ, v, t)
                 ~>
(λ  v     . b) t    SUBS(b, v, t)

Застосувати лямбда-абстракцію до терміна можна лише тоді, коли термін має той самий тип, що і змінна у відповідному кількісному показнику forall. Тоді ми зменшуємо абстракцію лямбда та квантор forall так само, як і раніше чисте лямбда-числення. Віднімаючи частину рівня значення, отримуємо правило (4) набору тексту.

Приклад

Ось оператор додатка функції:

∀ (A : *) (B : A -> *) (f : ∀ (y : A). B y) (x : A). B x
λ  A       B            f                    x     . f x

(Ми скорочуємо , ∀ (x : σ). τщоб σ -> τякщо τне згадувати x)

fповертає B yдля будь-якого наданого yтипу A. Застосуємо fдо x, що правильний тип A, і замінити yна xв AFTER ., таким чином , f x ∈ SUBS(B y, y, x)~> f x ∈ B x.

Тепер скоротимо оператор додатка функції як appі застосуємо його до себе:

∀ (A : *) (B : A -> *). ?
λ  A       B          . app ? ? (app A B)

Я розміщую ?умови, які нам потрібно надати. По- перше , ми явно вводимо і Instantiate Aі B:

∀ (f : ∀ (y : A). B y) (x : A). B x
app A B

Тепер нам потрібно уніфікувати те, що ми маємо

∀ (f : ∀ (y : A). B y) (x : A). B x

що те саме

(∀ (y : A). B y) -> ∀ (x : A). B x

і що app ? ?отримує

∀ (x : A'). B' x

Це призводить до

A' ~ ∀ (y : A). B y
B' ~ λ _. ∀ (x : A). B x -- B' ignores its argument

(див. також Що таке предикативність? )

Наш вираз (після деякого перейменування) стає

∀ (A : *) (B : A -> *). ?
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

Так як для будь-якого A, Bі f(де f ∈ ∀ (y : A). B y)

∀ (y : A). B y
app A B f

ми можемо ініціювати Aта Bотримати (для будь-якого fз відповідним типом)

∀ (y : ∀ (x : A). B x). ∀ (x : A). B x
app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) f

і підпис типу еквівалентний (∀ (x : A). B x) -> ∀ (x : A). B x.

Весь вираз є

∀ (A : *) (B : A -> *). (∀ (x : A). B x) -> ∀ (x : A). B x
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

Тобто

∀ (A : *) (B : A -> *) (f : ∀ (x : A). B x) (x : A). B x
λ  A       B            f                    x     .
    app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B) f x

що після всіх зменшень на рівні значень дає ту саму appназад.

Таким чином , в той час як він вимагає всього кілька кроків в чистому лямбда - обчисленні , щоб отримати appвідapp app , в типизированной настройки (і особливо в залежності надрукував) ми також повинні дбати про об'єднання і речі стають все більш складними , навіть з деяким inconsitent зручності ( * ∈ *).

Перевірка типу

  • Якщо tє , *тоt ∈ * з (1)
  • Якщо tє ∀ (x : σ) τ, σ ∈? *, τ ∈? *(дивіться примітка про ∈?нижче) , тоt ∈ * в силу (2)
  • Якщо tє f x, f ∈ ∀ (v : σ) τдля деяких σі τ,x ∈? σ то в t ∈ SUBS(τ, v, x)силу (4)
  • Якщо tце змінна v, vвведена до того ∀ (v : σ). τчасуt ∈ σ (5)

Це все правила виводу, але ми не можемо зробити те ж саме для лямбдів (умовивід про тип не залежить від залежних типів). Тож для лямбдів перевіряємо (t ∈? σ ), а не висновок:

  • Якщо tє λ v. bі перевірено ∀ (v : σ) τ,b ∈? τ тоt ∈ ∀ (v : σ) τ
  • Якщо tце щось інше і перевірено, σтоді слід зробити висновок про тип tвикористання вищезгаданої функції та перевірити, чи є вонаσ

Перевірка рівності для типів вимагає, щоб вони були у звичайних формах, тому для вирішення, чи tє тип, σми спочатку перевіряємо, що σмає тип *. Якщо так, то σце нормалізується (парадокс модуля Жирара) і він нормалізується (відтак σстає добре сформованим через (0)). SUBSтакож нормалізує вирази для збереження (0).

Це називається двонаправлена ​​перевірка типу. З його допомогою нам не потрібно коментувати кожну лямбда з типом: якщо f xтип типу fвідомий, то xвін перевіряється на тип аргументу, який fотримує, замість того, щоб робити висновок і порівнювати за рівність (що також менш ефективно). Але якщо fце лямбда, вона вимагає явного анотації типу (анотації опущені в граматиці та скрізь, ви можете додатиAnn Term Term або λ' (σ : Term) (v : Var)до конструкторів).

Крім того, подивіться на простіше, легше! блогпост.


1
Виділення "Простіше, легше".

Перше правило скорочення для forall виглядає дивно. На відміну від лямбда, foralls не слід застосовувати добре набраним способом (правда?).

@chi, я не розумію, що ти говориш. Можливо, моє позначення погано: правило зменшення говорить (λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)~> SUBS(b, v, t) ∈ SUBS(τ, v, t).
користувач3237465

1
Я вважаю, що позначення вводить в оману. Схоже, у вас було два правила: одне для дурниць, (∀ (v : σ). τ) t ~> ...а інше - для змістовного (λ v. b) t ~> .... Я б видалив перший і перетворив би його на коментар нижче.

1
Правило (1) містить свій висновок як передумови. Ви можете порівняти простоту вашої системи з двонаправленою версією лише після того, як у вас є система, яка працює. Ви можете сказати, що ви тримаєте все нормалізовано, але ваші правила цього не роблять.

24

Давайте поїдемо. Я не буду морочитися парадоксом Жирара, оскільки він відволікає від центральних ідей. Мені потрібно буде представити деякі презентаційні механізми щодо суджень та виводів тощо.

Граматика

Термін :: = (Elim) | * | (Var: Термін) → Term | λVar↦Term

Elim :: = Термін: Термін | Вар | Термін Elim

Граматика має дві взаємно визначені форми, "терміни", які є загальним поняттям речі (типи - речі, значення - речі), включаючи * (тип типів), залежні типи функцій та лямбда-абстракції, але також вбудовування " елімінації "(тобто" звичаї ", а не" конструкції "), які є вкладеними програмами, коли річ, зрештою, у функціональному положенні є або змінною, або терміном, позначеним своїм типом.

Правила скорочення

(λy↦t: (x: S) → T) s ↝ t [s: S / y]: T [s: S / x]

(t: T) ↝ t

Операція підстановки t [e / x] замінює кожне виникнення змінної x усуненням e, уникаючи захоплення імен. Для формування програми, яка може скоротити, лямбда- термін повинен бути анотований за його типом для усунення . Анотація типу надає лямбда-абстракції своєрідну "реактивність", що дозволяє додатку продовжуватися. Як тільки ми дійдемо до того, що більше не відбувається додатків, і активний t: T вбудовується назад у синтаксис терміна, ми можемо скинути анотацію типу.

Розширимо відношення ↝ скорочення за допомогою структурного закриття: правила застосовуються будь-де всередині термінів та виключень, що ви можете знайти щось, що відповідає лівій частині. Напишіть ↝ * для рефлексивно-перехідного (0 або більше крокового) закриття ↝. У цьому сенсі система відновлення є злитою:

Якщо s ↝ * p і s ↝ * q, то існує деякий r такий, що p ↝ * r і q ↝ * r.

Контексти

Контекст :: = | Контекст, Вар: Термін

Контексти - це послідовності, які присвоюють типи змінним, зростаючим праворуч, що ми думаємо як "локальний" кінець, розповідаючи нам про останні нещодавно пов'язані змінні. Важливою властивістю контекстів є те, що завжди можна вибрати змінну, яка вже не використовується в контексті. Ми стверджуємо, інваріант, що змінні типи, що приписуються в контексті, відрізняються.

Судження

Судження :: = Контекст ⊢ Термін має термін | Контекст ⊢ Елім - термін

Це граматика суджень, але як їх читати? Для початку ⊢ - традиційний символ "турнікет", який відокремлює припущення від висновків: ви можете читати його неофіційно як "каже".

G ⊢ T має t

означає, що заданий контекст G тип T допускає термін t;

G ⊢ e - S

означає, що для заданого контексту G усунення e задано типу S.

Судження мають цікаву структуру: нуль або більше входів , один або більше предметів , нуль або більше результатів .

INPUTS                   SUBJECT        OUTPUTS
Context |- Term   has    Term
Context |-               Elim      is   Term

Тобто ми повинні запропонувати типи термінів заздалегідь і просто перевірити їх, але ми синтезуємо типи вилучень.

Правила введення тексту

Я представляю їх у невиразно пролозькому стилі, пишучи J -: P1; ...; Pn, щоб вказати, що судження J має місце, якщо приміщення P1 - Pn також є. Передумовою буде ще одне рішення чи вимога про зменшення.

Умови

G ⊢ T має t -: T ↝ R; G ⊢ R має t

G ⊢ * має *

G ⊢ * має (x: S) → T -: G ⊢ * має S; G, z: S! - * має T [z / x]

G ⊢ (x: S) → T має λy↦t -: G, z: S ⊢ T [z / x] має t [z / y]

G ⊢ T має (e) -: G ⊢ e - T

Усунення

G ⊢ e R -: G ⊢ e S; S ↝ R

G, x: S, G '⊢ x - S

G ⊢ fs є T [s: S / x] -: G ⊢ f є (x: S) → T; G ⊢ S має s

І це все!

Тільки два правила не спрямовані на синтаксис: правило, яке говорить "ви можете зменшити тип перед тим, як використовувати його для перевірки терміна", і правило, яке говорить "ви можете зменшити тип після того, як ви його синтезували з усунення". Однією життєздатною стратегією є зменшення типів до тих пір, поки ви не відкриєте найвищий конструктор.

Ця система не сильно нормалізується (через Парадокс Жирара, який є парадоксалом самонавідності у брехунському стилі), але це може бути зроблено сильно нормалізуючим шляхом розщеплення * на "рівні Всесвіту", де будь-які значення, що включають типи на самих нижчих рівнях мають типи на більш високих рівнях, що запобігає самонаправленню.

Однак у цій системі є властивість збереження типу.

Якщо G ⊢ T має t і G ↝ * D і T ↝ * R і t ↝ r, то D ⊢ R має r.

Якщо G ⊢ e S, G ↝ * D і e ↝ f, то існує R такий, що S ↝ * R і D ⊢ f є R.

Контексти можна обчислити, дозволивши обчислити умови, які вони містять. Тобто, якщо судження дійсне зараз, ви можете обчислити його вхід, скільки завгодно, та його предмет за один крок, і тоді можна буде якось обчислити його результати, щоб переконатися, що отримане судження залишається дійсним. Доказом є проста індукція на типізацію похідних, враховуючи злиття -> *.

Звичайно, тут я представив лише функціональне ядро, але розширення можуть бути досить модульними. Ось пари.

Термін :: = ... | (x: S) * T | с, т

Elim :: = ... | напр. | напр

(s, t: (x: S) * T) .head ↝ s: S

(s, t: (x: S) * T) .tail ↝ t: T [s: S / x]

G ⊢ * має (x: S) * T -: G ⊢ * має S; G, z: S ⊢ * має T [z / x]

G ⊢ (x: S) * T має s, t -: G ⊢ S має s; G ⊢ T [s: S / x] має t

G ⊢, наприклад, S -: G ⊢ e є (x: S) * T

G ⊢, наприклад, T [e.head / x] -: G ⊢ e є (x: S) * T


1
G, x:S, G' ⊢ x is S -: G' ⊬ x?
користувач3237465

1
@ user3237465 Ні. Спасибі! Виправлено. (Коли я замінював турнікети мистецтва Ascii на турнікети html (таким чином роблячи їх невидимими на своєму телефоні; вибачте, якщо це відбувається в інших місцях), я пропустив цей.)

1
О, я думав, ти просто вказуєш на друкарську помилку. Правило говорить, що для кожної змінної в контексті ми синтезуємо тип, який контекст присвоює їй. Вводячи контексти, я сказав: "Ми дотримуємось інваріантності того, що змінні типи, що приписуються в контексті, є різними". тому затінення заборонено. Ви побачите, що кожного разу, коли правила розширюють контекст, вони завжди вибирають свіжий "z", який створює всі ті, що в'яжуться. Затінення - анафема. Якщо у вас контекст x: *, x: x, тоді тип більш локального x більше не в порядку, оскільки це x поза сферою.

1
Я просто хотів, щоб ви та інші відповідачі знали, що я повертаюсь до цієї теми кожну перерву від роботи. Мені дуже хочеться це навчитися, і за першим ім’ям я впав так, як насправді отримую більшість цього. Наступним кроком буде реалізація та написання кількох програм. Я радий, що можу жити в епоху, коли інформація про такі чудові теми доступна по всьому світу для когось, як я, і це все завдяки геніям, як ви, які присвячуєте деякий час свого життя для поширення цих знань, безкоштовно, в Інтернеті. Пробачте ще раз за те, що ви дуже сильно сформулювали моє запитання, і дякую!
MaiaVictor

1
@cody Так, розширення немає. Щоб зрозуміти, чому це не потрібно, зауважте, що два правила обчислень дозволяють розгорнути стратегію, де ви повністю нормалізуєте типи перед тим, як перевірити терміни, а також нормалізуєте типи відразу після синтезування їх з усунення. Тож у правилі, де типи повинні відповідати, вони вже нормалізуються, отже, на носі рівні, якщо "оригінальні" перевірені та синтезовані типи були конвертованими. Тим часом обмеження перевірки рівності лише цим місцем нормально через цей факт: якщо Т конвертована в канонічний тип, вона зводиться до канонічного типу.
поросятка

8

Кореспонденція Кері-Говарда говорить, що існує систематичне відповідність між типами систем і системами доказування в логіці. Зважаючи на це, орієнтований на програміста, ви можете переробити це таким чином:

  • Логічні системи підтвердження - це мови програмування.
  • Ці мови набрані статично.
  • Відповідальність системи типу такою мовою полягає у забороні програм, які б створювали неправомірні докази.

Погляд з цього кута:

  • Нетипічне обчислення лямбда, яке ви підсумуєте, не має системи значущого типу, тому побудована на ньому система доказів була б незвучною.
  • Просто набране лямбда-числення - це мова програмування, яка має всі типи, необхідні для побудови звукоізоляції в сентенційній логіці ("якщо / тоді", "і", "або", "не"). Але його типи недостатньо хороші для перевірки доказів, які містять квантори ("для всіх х, ..."; "існує х такий, що ...").
  • Залежно введене лямбда-числення має типи та правила, які підтримують сентенційну логіку та квантори першого порядку (кількісне визначення значень).

Ось правила природної дедукції для логіки першого порядку, використовуючи діаграму із запису Вікіпедії щодо природного дедукції . Це, в основному, правила мінімально залежного набору лямбда.

Природні дедукції першого порядку

Зауважте, що правила містять лямбда-терміни в них. Їх можна читати як програми, які будують докази речень, представлених їх типами (або, більш коротко, ми просто кажемо, що програми є доказом ). Подібні правила скорочення, які ви надаєте, можуть застосовуватися до цих лямбда-термінів.


Чому нас це хвилює? Ну, по-перше, тому що докази цілком можуть виявитися корисним інструментом програмування, а наявність мови, яка може працювати з доказами як першокласні об'єкти, відкриває багато шляхів. Наприклад, якщо у вашої функції є передумова, замість того, щоб записувати її як коментар, ви насправді можете вимагати підтвердження цього як аргументу.

По-друге, тому що системна техніка типу, необхідна для обробки кількісних показників, може мати інші можливості використання в контексті програмування. Зокрема, мови, що залежать від типу, обробляють універсальні квантори ("для всіх х, ..."), використовуючи поняття, яке називається залежними типами функцій - функцією, де статичний тип результату може залежати від значення часу виконання часу аргументу.

Щоб надати це дуже пішохідному застосуванню, я весь час пишу код, який повинен читати файли Avro, що складаються з записів з однаковою структурою - всі мають однаковий набір полів імена та типи. Це вимагає від мене:

  1. Жорсткий код структури записів у програмі як тип запису.
    • Переваги: ​​код простіший, і компілятор може вловлювати помилки в моєму коді
    • Недолік: Програма важко кодує для читання файлів, які відповідають типі запису.
  2. Читайте схему даних під час виконання, представляйте її загально як структуру даних та використовуйте її для загальної обробки записів.
    • Переваги: ​​Моя програма не кодується лише одним типом файлів
    • Недоліки: компілятор не може назбирати стільки помилок.

Як ви бачите на сторінці підручника Avro Java , вони показують, як користуватися бібліотекою відповідно до обох цих підходів.

Залежно від типів функцій ви можете мати свій торт і з'їсти його ціною більш складної системи. Ви можете написати функцію, яка читає файл Avro, витягує схему та повертає вміст файлу у вигляді потоку записів , статичний тип якого залежить від схеми, що зберігається у файлі . Компілятор міг би вловлювати помилки, коли я, наприклад, намагався отримати доступ до названого поля, яке може не існувати в записах файлів, які він обробляє під час виконання. Солодке, так?


1
Типи будівництва під час виконання у згаданій моді - це щось дійсно круте, про яке я не думав. Досить солодке, справді! Дякую за проникливу відповідь.
MaiaVictor
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.