Цепочка с помощью кратчайшего маршрута

Проблема

У меня есть набор типов и набор преобразований между ними. Это звучит как DAG и имеет некоторое сходство с ним. Я хотел бы иметь возможность вычислять неявно кратчайший путь преобразования между любыми двумя типами, если это возможно.

Я подготовил простой пример, который показывает мои тщетные попытки объявить такие импликации.

final case class A(u : Int)
final case class B(u : Int)
final case class BB(u : Int)
final case class C(u : Int)
final case class D(u: Int)

trait Convert[F,T] {
  def convert(source : F) : T
}

Я представляю следующие преобразования тестовых примеров: A → B, A → BB, B → C, B → D, C → D.

Я попробовал два подхода, и они дают мне разные неявные ошибки разрешения.

Транзитная цепочка

trait ConcreteConvert[F,T] extends Convert[F,T]

class Transit[F,M,T](implicit fm : ConcreteConvert[F,M], mt : Convert[M,T]) extends Convert[F,T] {
  override def convert(source : F) : T =
    mt.convert( fm.convert(source) )
}

object Implicits {
  implicit def transit[F,M,T](implicit fm : ConcreteConvert[F,M], mt : Convert[M,T]) : Convert[F,T] =
    new Transit()(fm, mt)

  implicit object A2B extends ConcreteConvert[A,B] {
    override def convert(source : A) : B = B(source.u)
  }
  implicit object B2C extends ConcreteConvert[B,C] {
    override def convert(source : B) : C = C(source.u)
  }
  /*implicit object A2BB extends ConcreteConvert[A,BB] {
    override def convert(source : A) : BB = BB(source.u)
   }*/ // compilation fails
  implicit object C2D extends ConcreteConvert[C,D] {
    override def convert(source : C) : D = D(source.u)
  }
  implicit object B2D extends ConcreteConvert[B,D] {
    override def convert(source : B) : D = D(source.u)
  }
}

object Usage {
  import Implicits._
  def conv[F,T](source : F)(implicit ev : Convert[F,T]) : T =
    ev.convert(source)

  val a = A(0)
  val b = conv[A,B](a)
  val c = conv[A,C](a)
  val d = conv[A,D](a)
}

Такой подход обеспечил возможное разрешение пути между A → B → C → D и A → B → D, компилятор выбирает последний маршрут. Но он терпит неудачу, если есть ветвление

Продолжение передачи

abstract class PostConvert[F, M, T](mt : Convert[M,T]) extends Convert[F,T] {
  def pre(source : F) : M

  override def convert(source : F) : T =
    mt.convert( pre(source) )
}

class IdConvert[F]() extends Convert[F,F] {
  override def convert(source : F) : F =
    source
}

object ImplicitsPost {
  implicit def idConvert[F] : Convert[F,F] =
    new IdConvert[F]()

  implicit def a2b[T](implicit mt : Convert[B,T]) = new PostConvert[A,B,T](mt) {
    override def pre(source : A) : B = B(source.u)
  }
  implicit def a2bb[T](implicit mt : Convert[BB,T]) = new PostConvert[A,BB,T](mt) {
    override def pre(source : A) : BB = BB(source.u)
  }
  implicit def b2c[T](implicit mt : Convert[C,T]) = new PostConvert[B,C,T](mt) {
    override def pre(source : B) : C = C(source.u)
  }
  implicit def c2d[T](implicit mt : Convert[D,T]) = new PostConvert[C,D,T](mt) {
    override def pre(source : C) : D = D(source.u)
  }
  /*implicit def b2d[T](implicit mt : Convert[D,T]) = new PostConvert[B,D,T](mt) {
    override def pre(source : B) : D  = D(source.u)
  }*/ // compiler fails
}

object UsagePost {
  import ImplicitsPost._
  def conv[F,T](source : F)(implicit ev : Convert[F,T]) : T =
    ev.convert(source)

  val a = A(0)
  val b = conv[A,B](a)
  val c = conv[A,C](a)
  val d = conv[A,D](a)
}

В этом случае компилятор может игнорировать несоответствующее преобразование A → BB. Но он не разрешает конфликт A → B → C → D и A → B → D

Что я ищу

Некоторые способы решения проблемы в общем виде. Я мог бы определить график отношений и позволить имплицитной механике выбрать самый короткий путь в нем. Было бы лучше, если бы я мог настроить каждый конверсионный вес, чтобы сделать A → B → D и A → C → D различимыми. Существует некоторая черная магия позади неявного приоритета разрешения, и я надеюсь, что это могло бы помочь.

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

Ответ 1

Короткий ответ

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

Практический ответ

Лучше всего было бы изменить компилятор scala для поддержки обратного отслеживания в неявном разрешении, которого должно быть достаточно для вашей первой реализации.

Длинный ответ

Я соврал, это должно быть возможно с текущим состоянием компилятора, но это будет не так хорошо, как эквивалентная программа Prolog, которую вы напишете, и, вероятно, выйдет за рамки "О, это должно быть весело писать на уровне типа";). Позвольте мне сначала перефразировать вашу проблему.

Учитывая несколько типов:

trait A; trait B; trait B; trait C; trait D

И ориентированный граф для этих типов:

trait Edge[X, Y]

def fact[X, Y] = new Edge[X, Y] {}

implicit val edge0: Edge[A, B]  = fact //   ( A )
implicit val edge1: Edge[A, BB] = fact //   ↓   ↓
implicit val edge2: Edge[B, C]  = fact // ( B ) BB
implicit val edge3: Edge[B, D]  = fact // ↓   ↓
implicit val edge4: Edge[C, D]  = fact // C → D

Найдите кратчайший путь между A и D с использованием неявного разрешения.

Чтобы понять, какая сложность в этой проблеме, полезно разложить ее на две части:

  • Поднимите эти implicit edges в представление уровня уровня графа, начиная с node A, например:

    A 
      :: (B
        :: (C :: (D :: HNil) :: HNil)
        :: (D :: HNil)
        :: HNil)
      :: (BB :: HNil)
      :: HNil
    
  • Сделайте тип BFS уровня для этого представления.

Удивительно, но 1. сложнее, чем это звучит, и что, поскольку scala неявное разрешение не выполняет обратного отсчета. Вам понадобится немного другое представление графика, чтобы это было возможно.

Одно решение, которое придерживается вашей первоначальной формулировки (один неявный на край), может заключаться в использовании техники, аналогичной описанной в в этом примере, который использует два trait EdgeLeft[X, Y] и trait EdgeRight[X, Y] вместо trait Edge[X, Y] и собирает все ребра графа в один HList, эффективно работающий вокруг отсутствия обратного отслеживания.

Вы также можете сделать свою жизнь намного проще, закодировав ваш график в представлении, расположенном ближе к матрице смежности, например, с помощью implicit fact: Neighbours[A, B :: BB :: HNil]. Но в любом случае, небольшое изменение в вашем графическом представлении должно быть достаточным, чтобы позволить вам построить структуру, эквивалентную приведенному выше изображению вашего графика.

Решение 2. не будет легким, но теоретически не должно быть сложнее писать чистый, уровень DFS уровня на следующем входе и поднимать его до уровня типа:

val input: List[Any] = (
  "A" 
    :: ("B"
      :: ("C" :: ("D" :: Nil) :: Nil)
      :: ("D" :: Nil)
      :: Nil)
    :: ("BB" :: Nil)
    :: Nil
)

def DFS(i: List[Any]): List[String] = ???

Ответ 2

Я не думаю, что можно решить эту проблему с помощью инструментов, доступных в настоящее время в Scala.

Позвольте отступить и спросить, как решить эту проблему в целом? Если вы думаете об этой проблеме на мгновение, вы поймете, что это эквивалентно решению кратчайшего пути на графике. В этом случае узлы графа являются конкретными классами (здесь A, B, BB, C, and D), а ребрами являются значения в классе Convert.

Стандартный способ решения кратчайшего пути - Dijkstra Algorithm, который просто сводится к поиску невзвешенных графов в ширину, что и есть мы имеем здесь. Итак, как мы можем выполнить поиск по этому графику в ширину?

Per wikipedia здесь находится псевдокод для поиска по ширине:

     1 Breadth-First-Search(Graph, root):
     2 
     3     for each node n in Graph:            
     4         n.distance = INFINITY        
     5         n.parent = NIL
     6 
     7     create empty queue Q      
     8 
     9     root.distance = 0
    10     Q.enqueue(root)                      
    11 
    12     while Q is not empty:        
    13     
    14         current = Q.dequeue()
    15     
    16         for each node n that is adjacent to current:
    17             if n.distance == INFINITY:
    18                 n.distance = current.distance + 1
    19                 n.parent = current
    20                 Q.enqueue(n)

В этом алгоритме есть два места, где нам нужно перечислить все узлы в графе, которые соответствуют некоторому предикату. В строке 3 нам нужно перечислить ВСЕ узлы в графе. Это в принципе можно сделать, и бесформенный предлагает путь вперед по этому, предполагая, что узлы образуют ADT, которые они легко могли.

Однако в строке 16 нам нужно перечислить соседние узлы к тому, который у нас есть. Для этого нам понадобится проанализировать все ребра на нашем графике, что влечет за собой перечисление всех имплицитов определенной формы: если A является нашим node, то мы хотим, чтобы все члены класса стилей соответствовали Convert[A,_].

Scala не дает возможности перечислить этих членов класса с помощью механизма implicits. Причина в том, что механизм запроса неявного позволяет вам запрашивать самое большее ОДНОГО неявного и если обнаруживаются какие-либо неоднозначные (то есть множественные равнозначные) импликации, то это считается ошибкой. Подумайте, что произойдет, если мы запросим все ребра для node B:

def соседнийToB [OtherNode] (неявные ребра: Convert [B, OtherNode]) = ребра

Поскольку вышеупомянутый вызов будет удовлетворен как Convert[B,C], так и Convert[B,D], компилятор даст нам ошибку.

Боковое примечание: вы можете подумать, что можете решить эту проблему, используя более глубокий поиск глубины в прологе. К сожалению, я думаю, что вы будете в тупике по той же проблеме.