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


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

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

Каковы некоторые практические и полезные примеры вариантов использования для зависимых типов методов, где они явно выгодны по сравнению с альтернативами?

какие интересные вещи мы можем сделать с ними, что не возможно/легко раньше?

что они покупают нам по сравнению с существующими функциями системы типов?

кроме того, зависимые типы методов аналогичны или черпают вдохновение из любых функций, найденных в системах типов других продвинутых типизированных языков, таких как Haskell, OCaml?

4 122

4 ответа:

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

так в чем проблема? Вложенные типы в 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, и больше ничего.

это бесспорно a желательное свойство, но предположим, что мы хотим переместить этот тестовый код в другой исходный файл? С зависимыми типами методов тривиально легко переопределить эти методы вне 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)

после недолгой борьбы с этим вы, вероятно, узнаете, почему я (или, может быть, это был Дэвид Макивер, мы не можем вспомнить, кто из нас придумал этот термин) называю это пекарней судьбы.

Edit: консенсус в том, что пекарня судьбы была Дэвидом Макивером чеканка. ..

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

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 и пишут расширение "pimp my library" для него.

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

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

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

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

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

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