Будь-яка причина, чому scala прямо не підтримує залежні типи?


109

Існують типи, що залежать від шляху, і я думаю, що можна висловити майже всі функції таких мов, як Epigram або Agda у Scala, але мені цікаво, чому Scala не підтримує це більш явно, як це дуже добре в інших областях (скажімо , DSL)? Щось мені не вистачає на кшталт "не треба"?


3
Що ж, дизайнери Scala вважають, що Ламбда-куб Barendregt - це не все-всьому в теорії типів. Це може бути або не може бути причиною.
Йорг W Міттаг

8
@ JörgWMittag Що таке Куб Ламди? Якийсь магічний пристрій?
Ашкан Х. Назарій

@ ashy_32bit див. статтю Барендрегта "Вступ до систем узагальненого типу" тут: diku.dk/hjemmesider/ansatte/henglein/papers/barendregt1991.pdf
iainmcgin

Відповіді:


151

Синтаксична зручність убік, поєднання однотонних типів, залежних від шляху типів та неявних значень означає, що Scala на диво добре підтримує залежне введення тексту, як я намагався продемонструвати безформним .

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

scala> class Foo { class Bar }
defined class Foo

scala> val foo1 = new Foo
foo1: Foo = Foo@24bc0658

scala> val foo2 = new Foo
foo2: Foo = Foo@6f7f757

scala> implicitly[foo1.Bar =:= foo1.Bar] // OK: equal types
res0: =:=[foo1.Bar,foo1.Bar] = <function1>

scala> implicitly[foo1.Bar =:= foo2.Bar] // Not OK: unequal types
<console>:11: error: Cannot prove that foo1.Bar =:= foo2.Bar.
              implicitly[foo1.Bar =:= foo2.Bar]

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

Однак часто заперечують, що Scala не є "повністю" мовою залежного типу, оскільки вона не має залежно від суми та типів продуктів, як це можна знайти в Agda, Coq або Idris як властивих. Я думаю, що це певною мірою відображає фіксацію форми над фундаментальними, однак я спробую показати, що Скала набагато ближче до цих інших мов, ніж зазвичай визнається.

Незважаючи на термінологію, типи залежних сум (також відомі як типи Sigma) - це просто пара значень, де тип другого значення залежить від першого значення. Це безпосередньо представлено в Scala,

scala> trait Sigma {
     |   val foo: Foo
     |   val bar: foo.Bar
     | }
defined trait Sigma

scala> val sigma = new Sigma {
     |   val foo = foo1
     |   val bar = new foo.Bar
     | }
sigma: java.lang.Object with Sigma{val bar: this.foo.Bar} = $anon$1@e3fabd8

насправді, це важлива частина кодування залежних типів методу, яка необхідна для виходу з "Пекарні Дум" у Скалі до 2.10 (або раніше за допомогою експериментального варіанту компілятора типів "Скала").

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

scala> trait Pi[T] { type U }
defined trait Pi

Ми можемо, ніж визначити поліморфний метод, який використовує цей тип,

scala> def depList[T](t: T)(implicit pi: Pi[T]): List[pi.U] = Nil
depList: [T](t: T)(implicit pi: Pi[T])List[pi.U]

(зверніть увагу на використання типу залежності від шляху у типі pi.Uрезультату List[pi.U]). Враховуючи значення типу T, ця функція поверне (n порожній) список значень типу, що відповідає цьому конкретному значенню T.

Тепер давайте визначимо деякі відповідні значення та неявні свідки для функціональних відносин, які ми хочемо мати,

scala> object Foo
defined module Foo

scala> object Bar
defined module Bar

scala> implicit val fooInt = new Pi[Foo.type] { type U = Int }
fooInt: java.lang.Object with Pi[Foo.type]{type U = Int} = $anon$1@60681a11

scala> implicit val barString = new Pi[Bar.type] { type U = String }
barString: java.lang.Object with Pi[Bar.type]{type U = String} = $anon$1@187602ae

А тепер наша функція використання типу Pi в дії,

scala> depList(Foo)
res2: List[fooInt.U] = List()

scala> depList(Bar)
res3: List[barString.U] = List()

scala> implicitly[res2.type <:< List[Int]]
res4: <:<[res2.type,List[Int]] = <function1>

scala> implicitly[res2.type <:< List[String]]
<console>:19: error: Cannot prove that res2.type <:< List[String].
              implicitly[res2.type <:< List[String]]
                    ^

scala> implicitly[res3.type <:< List[String]]
res6: <:<[res3.type,List[String]] = <function1>

scala> implicitly[res3.type <:< List[Int]]
<console>:19: error: Cannot prove that res3.type <:< List[Int].
              implicitly[res3.type <:< List[Int]]

(Зверніть увагу , що тут ми використовуємо в Scala <:<підтип-свідка оператор , а не =:=тому , що res2.typeі res3.typeє одноелементними типами і , отже , більш точними , ніж типів ми перевірочні на РІТ).

На практиці, однак, у Scala ми б не починали з кодування типів Sigma та Pi, а потім переходили звідти, як у Agda чи Idris. Натомість ми б використовували безпосередньо залежні від шляху типи, однотонні типи та імпліцити безпосередньо. Ви можете знайти численні приклади того, як це відбувається у безформних формах : типи розмірів , розширювані записи , вичерпні списки HL , брухт вашої котлової панелі , загальні блискавки тощо.

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

У будь-якому випадку, я не думаю, що це незначне обмеження домену може використовуватися як заперечення щодо статусу Scala як мови, що набирається залежно. Якщо це так, то те ж саме можна сказати і про залежний ML (який допускає лише залежності від натуральних значень чисел), що було б химерним висновком.


8
Майлз, спасибі за цю дуже детальну відповідь. Мені трохи цікаво одне. Жоден із ваших прикладів не здається на перший погляд особливо неможливим висловити в Haskell; Ви тоді стверджуєте, що Haskell також є мовою залежної типу?
Джонатан Стерлінг

8
Я прихильнився тому, що я не можу розрізнити методику тут, по суті, від методик, описаних у citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.22.2636 McBride ", тобто це способи моделювання залежні типи, не надавати їх безпосередньо.
sclv

2
@sclv Я думаю, ви пропустили, що Scala має залежні типи без будь-якої форми кодування: дивіться перший приклад вище. Ви абсолютно праві, що в моєму кодуванні типів Pi використовуються одні і ті ж методики, що і в папері Коннора, але з підкладки, яка вже включає типи, що залежать від шляху, і типи однотонних.
Майлз Сабін

4
Ні. Звичайно, ви можете мати типи, прив'язані до об'єктів (це наслідок об'єктів як модулів). Але ви не можете проводити обчислення цих типів без використання свідків рівня цінності. Насправді =: = сам є свідком рівня цінності! Ви все ще підробляєте це, як і в Haskell, чи, можливо, більше.
sclv

9
Scala's =: = не є значенням рівня, це конструктор типу - значення для цього є тут: github.com/scala/scala/blob/v2.10.3/src/library/scala/… , і не здається особливо відрізняється від свідка твердження про рівність у таких мовах залежно від типу, як Agda та Idris: refl. (Див. Www2.tcs.ifi.lmu.de/~abel/Equality.pdf розділ 2 та eb.host.cs.st-andrews.ac.uk/writings/idris-tutorial.pdf, розділ 8.1 відповідно)
pdxleif

6

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


Ви б були ласкаві, щоб дати мені деякий теоретичний досвід залежним типам (можливо, посилання)?
Ашкан Х. Назарій

3
@ ashy_32bit, якщо ви можете отримати доступ до "Розширених тем про типи та мови програмування" Бенджаміна Пірса, є розділ у цій частині, який дає розумне ознайомлення із залежними типами. Ви також можете прочитати деякі статті Конора Макбрайда, який особливо цікавиться залежними типами на практиці, а не в теорії.
iainmcgin

3

Я вважаю, що типи, що залежать від Шкала, можуть представляти лише Σ-типи, але не Π-типи. Це:

trait Pi[T] { type U }

не є точно Π -порядком. За визначенням, Π-тип або залежний продукт - це функція, тип результату залежить від значення аргументу, що представляє універсальний кількісний показник, тобто ∀x: A, B (x). Однак у вищезазначеному випадку це залежить лише від типу T, але не від якогось значення цього типу. Сама ознака пі - це Σ-тип, екзистенціальний квантор, тобто ∃x: A, B (x). Самопосилання на об'єкт у цьому випадку діє як кількісна змінна. Однак, коли він передається як неявний параметр, він зводиться до функції звичайного типу, оскільки він вирішується по типу. Кодування залежного продукту в Scala може виглядати наступним чином:

trait Sigma[T] {
  val x: T
  type U //can depend on x
}

// (t: T) => (∃ mapping(x, U), x == t) => (u: U); sadly, refinement won't compile
def pi[T](t: T)(implicit mapping: Sigma[T] { val x = t }): mapping.U 

Відсутня деталь тут - здатність статично обмежувати поле x до очікуваного значення t, ефективно формуючи рівняння, що представляє властивість усіх значень, що населяють тип Т. Разом з нашими Σ-типами, що використовуються для вираження існування об'єкта із заданою властивістю, формується логіка, в якій наше рівняння є теоремою, яку потрібно довести.

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


1
Майлз Сабін вище показав приклад використання Piдля створення типів залежно від значень.
зниклий фактор

У прикладі, depListвитяги типу Uз Pi[T], вибраних для типу (не значення) t. Цей тип просто буває однотонним, на даний момент доступним для одиночних об'єктів Scala і представляє їх точні значення. Приклад створює одну реалізацію для Piодиночного типу об'єкта, таким чином спарюючи тип зі значенням, як у Σ-тип. Π-type, з іншого боку, - це формула, яка відповідає структурі вхідного параметра. Можливо, у Scala їх немає, оскільки Π-типи вимагають, щоб кожен тип параметра був GADT, а Scala не відрізняв GADT від інших типів.
П. Фролов

Гаразд, я трохи розгублений. Чи не pi.Uв прикладі Майлза вважати залежним типом? Це на цінність pi.
зниклий фактор

2
Він справді вважається залежним типом, але є різні аромати з них: Σ-type ("існує x такий, що P (x)", логічно-розумний) та Π-тип ("для всіх x, P (x)") . Як ви зазначали, тип pi.Uзалежить від значення pi. Проблема, що заважає trait Pi[T]стати Π-типом, полягає в тому, що ми не можемо зробити це залежним від значення довільного аргументу (наприклад, tв depList), не піднімаючи цей аргумент на рівні типу.
П. Фролов

1

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

Це не спроба відповісти на питання, наскільки легко було б перетворити Scala на щось на кшталт Ідріса (я думаю, що це дуже важко) або написати бібліотеку, яка пропонує більш пряму підтримку таких можливостей Idris (як, наприклад, singletonsспроби бути в Haskell).

Натомість я хочу наголосити на прагматичній різниці між Скалою та такою мовою, як Ідріс.
Що таке біти коду для виразів рівня та значення? Ідріс використовує один і той же код, Scala використовує зовсім інший код.

Scala (подібно до Haskell), можливо, зможе кодувати безліч обчислень рівня типу. Це показано на зразок бібліотек shapeless. Ці бібліотеки роблять це, використовуючи деякі дійсно вражаючі та хитрі прийоми. Однак, код рівня їх типу (на даний момент) сильно відрізняється від виразів рівня значень (я вважаю, що цей розрив дещо ближчий у Haskell). Idris дозволяє використовувати вираження рівня значення на рівні типу AS IS.

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

Розглянемо підтверджений код. Дивіться:
https://github.com/idris-lang/Idris-dev/blob/v1.3.0/libs/contrib/Interfaces/Verified.idr
Перевірка типів перевіряє докази монадійних / функціональних / прикладних законів, а докази - про фактичні реалізації monad / functor / applicative, а не якийсь еквівалент рівня закодованого типу, який може бути однаковим чи не однаковим. Велике питання - що ми доводимо?

Те саме можна зробити, використовуючи розумні трюки кодування (див. Наступне для версії Haskell, я не бачив жодного для Scala)
https://blog.jle.im/entry/verified-instance-in-haskell.html
https: // github.com/rpeszek/IdrisTddNotes/wiki/Play_FunctorLaws,
за винятком типів, настільки складні, що важко зрозуміти закони, вирази рівня значень перетворюються (автоматично, але все-таки) для введення речей на рівні, і ви також повинні довіряти цій конверсії . У всьому цьому є місце для помилок, яке нібито не відповідає меті компілятора, який виступає помічником доказу.

(ЗДОРОВ'Я 2018.8.10) Якщо говорити про доказову допомогу, тут є ще одна велика різниця між Ідрісом та Скалою. У Скалі (або Хаскеллі) немає нічого, що може завадити написанню розбіжних доказів:

case class Void(underlying: Nothing) extends AnyVal //should be uninhabited
def impossible() : Void = impossible()

в той час як в Ідріса є такий totalкод, що запобігає компіляції такого коду.

Бібліотека Scala, яка намагається уніфікувати код і значення рівня (наприклад, Haskell singletons), була б цікавим тестом для підтримки Scala залежних типів. Чи можна подібну бібліотеку зробити набагато краще в Scala через типи, що залежать від шляху?

Я занадто новачок у Скалі, щоб сам відповісти на це питання.

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