Хтось може пояснити мені залежне друкування? Я маю невеликий досвід роботи з Haskell, Cayenne, Epigram чи іншими функціональними мовами, тому чим простіші терміни ви можете використовувати, тим більше я це оціню!
Хтось може пояснити мені залежне друкування? Я маю невеликий досвід роботи з Haskell, Cayenne, Epigram чи іншими функціональними мовами, тому чим простіші терміни ви можете використовувати, тим більше я це оціню!
Відповіді:
Враховуйте це: у всіх гідних мовах програмування ви можете писати функції, наприклад
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
приклад на самому ділі Доопрацювання типу, хоча? Це "досить близько", але не зовсім той тип "залежних типів", про який, наприклад, Ідріс згадує спочатку у підручнику про деп.типізацію.
Залежні типи дозволяють усунути більший набір логічних помилок під час компіляції . Для ілюстрації цього розглянемо наступну специфікацію функції 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
отже, термін залежні типи .
f(random())
призведе до помилки компіляції?
f
до якогось виразу вимагало б від компілятора (з вашою допомогою чи без неї) забезпечити, щоб вираз був завжди парним, і такого доказу не існує random()
(оскільки він насправді може бути непарним), тому f(random())
не зможе скомпілювати.
Якщо ви випадково знаєте 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), є саме тим, що означає залежний тип.
Причина, по якій ви часто не бачите цю функцію, полягає в тому, що вона відкриває велику банку хробаків: ви раптом потрапляєте в ситуації, коли, щоб перевірити під час компіляції, чи однакові два типи, вам доведеться довести два вирази еквівалентні (завжди матимуть одне і те ж значення під час виконання). Як результат, якщо порівняти список вікіпедійних мов, що вводяться залежно від типу, із переліком доказових теорем, ви можете помітити підозрілу подібність. ;-)
Цитування книги «Типи книг та мови програмування» (30.5):
Значна частина цієї книги стосується формалізації різних видів механізмів абстракції. У просто набраному лямбда-обчисленні ми формалізували операцію взяття терміна та абстрагування субтерма, отримуючи функцію, яку пізніше можна створити, застосувавши її до різних термінів. У системі ми рекапітулювали механізми просто набраного лямбда-числення "на один рівень вище", взявши тип і абстрагуючи підвираз, щоб отримати оператор типу, який пізніше можна створити за допомогою його застосування до різних типів. Зручний спосіб мислення всіх цих форм абстракції полягає в термінах сімейств виразів, індексованих іншими виразами. Звичайна лямбда-абстракція - це сімейство термінів, що індексуються термінами . Так само абстракція типу
F
ми розглянули операцію взяття терміна та абстрагування типу, отримавши термін, який можна створити за допомогою застосування до різних типів. Вλω
λx:T1.t2
[x -> s]t1
s
λX::K1.t2
є сімейством термінів, індексованих за типами, а оператор типів - це сімейство типів, індексованих за типами.
λx:T1.t2
родина термінів, індексованих термінами
λX::K1.t2
сімейство термінів, індексованих за видами
λX::K1.T2
сімейство типів, індексованих за типамиПереглядаючи цей список, стає зрозуміло, що є одна можливість, яку ми ще не розглядали: сімейства типів, проіндексовані термінами. Ця форма абстракції також широко вивчалася під рубрикою залежних типів.