Що таке залежне друкування?


85

Хтось може пояснити мені залежне друкування? Я маю невеликий досвід роботи з Haskell, Cayenne, Epigram чи іншими функціональними мовами, тому чим простіші терміни ви можете використовувати, тим більше я це оціню!


То що саме ви не зрозуміли, наприклад, про статтю Вікіпедії?
Карл Кнехтель

129
Ну, стаття відкривається кубиками лямбда, які для мене звучать як якийсь овечий м’ясо. Потім далі обговорюються системи λΠ2, і оскільки я не розмовляю чужоземно, я пропустив цей розділ. Потім я читав про числення індуктивних конструкцій, яке, до речі, мало що пов’язане з численням, теплообміном чи конструкцією. Після подання таблиці порівняння мов стаття закінчується, і я залишаюся більш розгубленим, ніж коли потрапив на сторінку.
Нік

4
@Nick Це загальна проблема Вікіпедії. Я бачив ваш коментар кілька років тому, і з тих пір пам’ятаю його. Зараз я роблю закладки.
Daniel H

Відповіді:


116

Враховуйте це: у всіх гідних мовах програмування ви можете писати функції, наприклад

def f(arg) = result

Тут fприймає значення argі обчислює значення result. Це функція від значень до значень.

Тепер деякі мови дозволяють визначати поліморфні (вони ж загальні) значення:

def empty<T> = new List<T>()

Тут emptyбере тип Tі обчислює значення. Це функція від типів до значень.

Зазвичай ви також можете мати загальні визначення типу:

type Matrix<T> = List<List<T>>

Це визначення приймає тип, і воно повертає тип. Його можна розглядати як функцію від типів до типів.

Стільки про те, що пропонують звичайні мови. Мова називається залежним типом, якщо вона також пропонує четверту можливість, а саме визначає функції від значень до типів. Або іншими словами, параметризація визначення типу над значенням:

type BoundedInt(n) = {i:Int | i<=n}

Деякі основні мови мають якусь фальшиву форму цього, яку не слід плутати. Наприклад, на C ++, шаблони можуть приймати значення як параметри, але вони повинні бути константами часу компіляції при застосуванні. Не так у мові, яка справді залежить. Наприклад, я міг би використати наведений вище тип так:

def min(i : Int, j : Int) : BoundedInt(j) =
  if i < j then i else j

Тут тип результату функції залежить від фактичного значення аргументу j, отже, від термінології.


Хіба це не BoundedIntприклад на самому ділі Доопрацювання типу, хоча? Це "досить близько", але не зовсім той тип "залежних типів", про який, наприклад, Ідріс згадує спочатку у підручнику про деп.типізацію.
Нарфанар

4
@Neein, уточнення типів справді є простою формою залежних типів.
Андреас Росберг,

23

Залежні типи дозволяють усунути більший набір логічних помилок під час компіляції . Для ілюстрації цього розглянемо наступну специфікацію функції f:

Функція fповинна приймати в якості вхідних даних лише парні цілі числа.

Без залежних типів ви можете зробити щось подібне:

def f(n: Integer) := {
  if  n mod 2 != 0 then 
    throw RuntimeException
  else
    // do something with n
}

Тут компілятор не може виявити, чи nсправді парний, тобто з точки зору компілятора наступний вираз нормальний:

f(1)    // compiles OK despite being a logic error!

Ця програма буде запускатися, а потім викидати виняток під час виконання, тобто у вашої програми є логічна помилка.

Тепер залежні типи дозволяють бути набагато виразнішими і дозволяють писати щось подібне:

def f(n: {n: Integer | n mod 2 == 0}) := {
  // do something with n
}

Тут nзалежний тип {n: Integer | n mod 2 == 0}. Можливо, допоможе прочитати це вголос як

n є членом набору цілих чисел таким чином, що кожне ціле число ділиться на 2.

У цьому випадку компілятор виявляє під час компіляції логічну помилку, де ви передали непарне число, fі перешкоджає виконанню програми:

f(1)    // compiler error

Ось наочний приклад використання типів, залежних від шляху Scala, як ми можемо спробувати реалізувати функцію, що fзадовольняє такій вимозі:

case class Integer(v: Int) {
  object IsEven { require(v % 2 == 0) }
  object IsOdd { require(v % 2 != 0) }
}

def f(n: Integer)(implicit proof: n.IsEven.type) =  { 
  // do something with n safe in the knowledge it is even
}

val `42` = Integer(42)
implicit val proof42IsEven = `42`.IsEven

val `1` = Integer(1)
implicit val proof1IsOdd = `1`.IsOdd

f(`42`) // OK
f(`1`)  // compile-time error

Головне помітити, як значення nвідображається у типі вартості, proofа саме n.IsEven.type:

def f(n: Integer)(implicit proof: n.IsEven.type)
      ^                           ^
      |                           |
    value                       value

Ми говоримо, що тип n.IsEven.type залежить від значення, n отже, термін залежні типи .


6
Як це стосується випадкової величини? Наприклад, це f(random())призведе до помилки компіляції?
Вонг Цзя Хау

7
Застосування fдо якогось виразу вимагало б від компілятора (з вашою допомогою чи без неї) забезпечити, щоб вираз був завжди парним, і такого доказу не існує random()(оскільки він насправді може бути непарним), тому f(random())не зможе скомпілювати.
Matthijs

19

Якщо ви випадково знаєте C ++, легко навести мотиваційний приклад:

Скажімо, у нас є тип контейнера та два його екземпляри

typedef std::map<int,int> IIMap;
IIMap foo;
IIMap bar;

і розглянемо цей фрагмент коду (можна припустити, що foo не порожній):

IIMap::iterator i = foo.begin();
bar.erase(i);

Це очевидне сміття (і, мабуть, пошкоджує структури даних), але воно буде перевіряти тип просто, оскільки "ітератор у foo" та "ітератор у бар" однакові IIMap::iterator, хоча вони семантично несумісні.

Проблема полягає в тому, що тип ітератора не повинен залежати лише від типу контейнера, а насправді від об’єкта контейнера , тобто він повинен бути "нестатичним типом члена":

foo.iterator i = foo.begin();
bar.erase(i);  // ERROR: bar.iterator argument expected

Така особливість, здатність виражати тип (foo.iterator), який залежить від терміна (foo), є саме тим, що означає залежний тип.

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


4

Цитування книги «Типи книг та мови програмування» (30.5):

Значна частина цієї книги стосується формалізації різних видів механізмів абстракції. У просто набраному лямбда-обчисленні ми формалізували операцію взяття терміна та абстрагування субтерма, отримуючи функцію, яку пізніше можна створити, застосувавши її до різних термінів. У системі ми рекапітулювали механізми просто набраного лямбда-числення "на один рівень вище", взявши тип і абстрагуючи підвираз, щоб отримати оператор типу, який пізніше можна створити за допомогою його застосування до різних типів. Зручний спосіб мислення всіх цих форм абстракції полягає в термінах сімейств виразів, індексованих іншими виразами. Звичайна лямбда-абстракція - це сімейство термінів, що індексуються термінами . Так само абстракція типу F ми розглянули операцію взяття терміна та абстрагування типу, отримавши термін, який можна створити за допомогою застосування до різних типів. Вλωλx:T1.t2[x -> s]t1sλX::K1.t2 є сімейством термінів, індексованих за типами, а оператор типів - це сімейство типів, індексованих за типами.

  • λx:T1.t2 родина термінів, індексованих термінами

  • λX::K1.t2 сімейство термінів, індексованих за видами

  • λX::K1.T2 сімейство типів, індексованих за типами

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

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