Які існують переконливі випадки використання залежних типів методу?


127

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

На перший погляд, не відразу очевидно, для чого це може бути корисно. Хайко Seeberger опублікував простий приклад залежних типів методів тут , які , як можна бачити , в коментарі може бути легко відтворений з параметрами типу по методам. Тож це був не дуже вагомий приклад. (Я можу пропустити щось очевидне. Будь ласка, виправте мене, якщо так.)

Назвіть кілька практичних та корисних прикладів випадків використання для залежних типів методів, коли вони, очевидно, вигідніші за альтернативи?

Що цікавого ми можемо зробити з ними, що раніше не було можливо / просто?

Що вони купують у нас через існуючі функції системи?

Також, чи залежні типи методів аналогічні або черпають натхнення від будь-яких особливостей, знайдених у системах типів інших вдосконалених набраних мов, таких як Haskell, OCaml?



Дякую за посилання, Ден! Я знаю про залежні типи загалом, але концепція залежних типів методів для мене відносно нова.
зниклий фактор

Мені здається, що "залежні типи методів" - це просто типи, які залежать від одного або декількох типів введення методу (включаючи тип об'єкта, на який використовується метод); нічого божевільного там поза загальною ідеєю залежних типів. Можливо, мені щось не вистачає?
Ден Бертон

Ні, ви цього не зробили, але, мабуть, я це зробив. :-) Я раніше не бачив зв’язку між ними. Хоча зараз це кристально ясно.
зниклий фактор

Відповіді:


112

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

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

Я проілюструю, що за допомогою вправи, яку я надаю під час мого курсу підвищення кваліфікації Scala ,

trait ResourceManager {
  type Resource <: BasicResource
  trait BasicResource {
    def hash : String
    def duplicates(r : Resource) : Boolean
  }
  def create : Resource

  // Test methods: exercise is to move them outside ResourceManager
  def testHash(r : Resource) = assert(r.hash == "9e47088d")  
  def testDuplicates(r : Resource) = assert(r.duplicates(r))
}

trait FileManager extends ResourceManager {
  type Resource <: File
  trait File extends BasicResource {
    def local : Boolean
  }
  override def create : Resource
}

class NetworkFileManager extends FileManager {
  type Resource = RemoteFile
  class RemoteFile extends File {
    def local = false
    def hash = "9e47088d"
    def duplicates(r : Resource) = (local == r.local) && (hash == r.hash)
  }
  override def create : Resource = new RemoteFile
}

Це приклад класичного шаблону тортів: у нас є сімейство абстракцій, які поступово вдосконалюються через герархію ( ResourceManager/ Resourceудосконалюються FileManager/ Fileякі в свою чергу уточнюються NetworkFileManager/ RemoteFile). Це іграшковий приклад, але візерунок справжній: він використовується у всьому компіляторі Scala і широко використовувався в плагіні Scala Eclipse.

Ось приклад використання абстракції,

val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)

Зауважте, що залежність шляху означає, що компілятор гарантуватиме, що testHashі testDuplicatesметоди на, NetworkFileManagerможна викликати лише аргументи, які відповідають йому, тобто. це власне RemoteFiles, і більше нічого.

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

def testHash4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.hash == "9e47088d")

def testDuplicates4(rm : ResourceManager)(r : rm.Resource) = 
  assert(r.duplicates(r))

Зауважте тут використання залежних типів методу: тип другого аргументу ( rm.Resource) залежить від значення першого аргументу ( rm).

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

Спробуйте самі ...

// Reimplement the testHash and testDuplicates methods outside
// the ResourceManager hierarchy without using dependent method types
def testHash        // TODO ... 
def testDuplicates  // TODO ...

testHash(rf)
testDuplicates(rf)

Через короткий час, бореться з цим, ви, мабуть, дізнаєтесь, чому я (а може, це був Девід Маківер, ми не можемо згадати, хто з нас ввів цей термін) називаю це «Пекарня долі».

Редагувати: консенсус полягає в тому, що Bakery of Doom був монетом Девіда Маківера ...

Для бонусу: Форма залежних типів Scala в цілому (і залежні типи методів як її частина) була натхнена мовою програмування Beta ... вони, природно, виникають із послідовної семантики гніздування Beta. Я не знаю жодної іншої, навіть слабкої основної мови програмування, яка має залежні типи в цій формі. Такі мови, як Coq, Cayenne, Epigram та Agda, мають різну форму залежної типізації, яка певною мірою є загальнішою, але яка значно відрізняється тим, що є частиною типних систем, які, на відміну від Scala, не мають підтипів.


2
Саме Девід Маківер придумав цей термін, але, у будь-якому випадку, це досить описово. Це фантастичне пояснення того, чому залежні типи методів такі захоплюючі. Хороша робота!
Даніель Шпієк

Він спочатку з’явився в розмові між нами двома на #scala досить давно ... наче я сказав, що не пам'ятаю, хто з нас це сказав хто першим.
Майлз Сабін

Здається, моя пам’ять зіграла на мене трюки ... консенсус - це монета на Девіда Маківера.
Майлз Сабін

Так, я тоді не був там (на #scala), але Хорхе був і саме там я отримував свою інформацію.
Даніель Шпієк

Використовуючи вдосконалення абстрактного типу, я зміг досить безболісно реалізувати функцію testHash4. def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")Я вважаю, що це можна вважати іншою формою залежних типів.
Марко ван Гільст

53
trait Graph {
  type Node
  type Edge
  def end1(e: Edge): Node
  def end2(e: Edge): Node
  def nodes: Set[Node]
  def edges: Set[Edge]
}

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

def shortestPath(g: Graph)(n1: g.Node, n2: g.Node) = ... 

Звичайно, це вже спрацювало, якщо визначено всередині Graph, але скажімо, що ми не можемо змінювати Graphі пишемо розширення "сутенер моя бібліотека".

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


6

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

trait C[A]
def f[M](a: C[M], b: M) = b
class C1 extends C[Int]
class C2 extends C[String]

f(new C1, 0)
res0: Int = 0
f(new C2, "")
res1: java.lang.String = 
f(new C1, "")
error: type mismatch;
 found   : C1
 required: C[Any]
       f(new C1, "")
         ^

Це не пов'язано. З членами типу ви можете використовувати уточнення для того ж результату: trait C {type A}; def f[M](a: C { type A = M}, b: M) = 0;class CI extends C{type A=Int};class CS extends C{type A=String}тощо
nafg

У будь-якому випадку це не має нічого спільного із залежними типами методів. Візьмемо для прикладу Олексія ( stackoverflow.com/a/7860821/333643 ). Використання вашого підходу (включаючи уточнену версію, яку я коментував) не досягає мети. Це забезпечить n1.Node =: = n2.Node, але не забезпечить, щоб вони були в одному графіку. IIUC DMT це забезпечує.
nafg

@nafg Дякуємо, що вказали на це. Я додав слово конкретний, щоб було зрозуміло, що я не мав на увазі випадку уточнення для членів типу. Наскільки я можу бачити, це все-таки є дійсним випадком використання для залежних типів методів, незважаючи на вашу думку (про що я знав), що вони можуть мати більше влади в інших випадках використання. Або я пропустив фундаментальну суть вашого другого коментаря?
Шелбі Мур III

3

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

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

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

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

trait Env {
...
  def callit[A](func: Env => Any => A, arg1key: String): A
  def callit[A](func: Env => Any => Any => A, arg1key: String, arg2key: String): A
}

Хоча я не перевіряв наступне, теоретично я можу отримати хеш-ключі від імен класів під час виконання роботи classOf, тому хеш-ключ - це ім'я класу, а не рядок (використовуючи посилання Scala для вставки рядка в ім'я класу).

trait DependentHashKey {
  type ValueType
}
trait `the hash key string` extends DependentHashKey {
  type ValueType <: SomeType
}

Так досягається статична безпека.

def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A

Коли нам потрібно передавати аргументальні ключі в одному значенні, я не тестував, але припускаю, що ми можемо використовувати Tuple, наприклад, для перевантаження 2 аргументів def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A. Ми б не використовували колекцію аргументальних ключів, тому що типи елементів будуть замінені (невідомі під час компіляції) у тип колекції.
Шелбі Мур III,

"статичне введення типу елемента хеш-картки підпорядковується Any або AnyRef" - я не дотримуюся. Коли ви говорите тип елемента, ви маєте на увазі тип ключа або тип значення (тобто аргумент першого чи другого типу для HashMap)? І навіщо це впасти?
Робін Грін

@RobinGreen Тип значень у хеш-таблиці. Afair, підписаний тому, що ви не можете розмістити більше одного типу в колекції в Scala, якщо ви не підключитесь до їх загального супертипу, оскільки у Scala немає типу об'єднання (диз'юнкції). Дивіться мої запитання щодо підписки в Scala.
Шелбі Мур III,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.