Каковы некоторые убедительные варианты использования зависимых типов методов?

Типы зависимых методов, которые раньше были экспериментальной функцией, теперь включены по умолчанию в соединительной линии, и, по-видимому, это кажется создали некоторое волнение в сообществе Scala.

После первого взгляда не сразу видно, что это может быть полезно для. Heiko Seeberger опубликовал простой пример зависимых типов методов здесь, что, как можно видеть в комментарии, можно легко воспроизвести с помощью параметров типа на методах. Так что это был не очень убедительный пример. (Возможно, я пропустил что-то очевидное. Пожалуйста, поправьте меня, если да.)

Каковы практические и полезные примеры использования для зависимых типов методов, где они явно выгодны по сравнению с альтернативами? Какие интересные вещи мы можем сделать с ними, что было невозможно/раньше? Что они покупают у нас по существующим функциям системы типов?

Спасибо!


ВОПРОС БОНУСА: Являются ли зависимые типы методов аналогичными/привлекают вдохновение от любых функций, обнаруженных в системах типов других продвинутых типизированных языков, таких как Haskell, OCaml?

Ответы

Ответ 1

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

Так в чем проблема? Вложенные типы в 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... они естественным образом возникают из согласованной семантики вложенности Бета. Я не знаю ни одного другого, даже слабого основного языка программирования, который имеет зависимые типы в этой форме. Языки, такие как Coq, Cayenne, Epigram и Agda, имеют различную форму зависимой типизации, которая в некотором роде более общая, но которая существенно отличается, будучи частью систем типов, которые, в отличие от Scala, не имеют подтипов.

Ответ 2

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 для аромата что.) Я не думаю, что раньше видел аналогию.

Ответ 3

Эта новая функция необходима, когда конкретные элементы типа используются вместо параметров типа. Когда используются параметры типа, зависимость типа семейного полиморфизма может быть выражена в последней и некоторых более старых версиях 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, "")
         ^

Ответ 4

Я разрабатывает модель для взаимодействия формы декларативного программирования с экологическим состоянием. Подробности здесь не актуальны (например, подробности о обратных вызовах и концептуальном сходстве с моделью Actor в сочетании с сериализатором).

Соответствующая проблема - это значения состояния, которые хранятся в хэш-карте и обозначаются значением хэш-ключа. Функции вводят неизменяемые аргументы, которые являются значениями из среды, могут вызывать другие такие функции и записывать состояние в среду. Но функциям не разрешено считывать значения из среды (поэтому внутренний код функции не зависит от порядка изменений состояния и, следовательно, остается декларативным в этом смысле). Как ввести это в Scala?

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

Но если эти хеш-ключи являются строками или целыми значениями хэша, статическая типизация типа элемента хэш-карты приближается к Any или 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