Возможно ли в Scala заставить вызывающего пользователя указать параметр типа для полиморфного метода?

//API
class Node
class Person extends Node

object Finder
{
  def find[T <: Node](name: String): T = doFind(name).asInstanceOf[T]
}

//Call site (correct)
val person = find[Person]("joe")

//Call site (dies with a ClassCast inside b/c inferred type is Nothing)
val person = find("joe")

В приведенном выше коде клиентский сайт "забыл" указать параметр типа, как автор API, я хочу, чтобы это означало "just return Node". Есть ли способ определить общий метод (а не класс) для достижения этого (или эквивалентного). Обратите внимание: использование манифеста внутри реализации для выполнения приведения if (manifest!= scala.reflect.Manifest.Nothing) не будет компилироваться... У меня есть ощущение, что некоторые мастера w61 знают, как использовать Predef. <: < для этого: -)

Идеи?

Ответы

Ответ 1

Еще одно решение - указать тип по умолчанию для параметра следующим образом:

object Finder {
   def find[T <: Node](name: String)(implicit e: T DefaultsTo Node): T = 
      doFind(name).asInstanceOf[T]
}

Ключ должен определить следующий тип phantom, чтобы действовать как свидетель по умолчанию:

sealed class DefaultsTo[A, B]
trait LowPriorityDefaultsTo {
   implicit def overrideDefault[A,B] = new DefaultsTo[A,B]
}
object DefaultsTo extends LowPriorityDefaultsTo {
   implicit def default[B] = new DefaultsTo[B, B]
}

Преимущество такого подхода заключается в том, что он полностью избегает ошибки (как во время выполнения, так и во время компиляции). Если вызывающий не указывает параметр типа, по умолчанию он равен Node.

Объяснение

Подпись метода find гарантирует, что он может быть вызван только в том случае, если вызывающий может предоставить объект типа DefaultsTo[T, Node]. Разумеется, методы default и overrideDefault упрощают создание такого объекта для любого типа T. Поскольку эти методы неявны, компилятор автоматически обрабатывает бизнес для вызова одного из них и передачи результата в find.

Но как компилятор знает, какой метод вызывать? Он использует свои правила вывода и неявные правила разрешения для определения соответствующего метода. Существует три случая:

  • find вызывается без параметра типа. В этом случае следует ввести тип T. Поиск неявного метода, который может предоставить объект типа DefaultsTo[T, Node], компилятор находит default и overrideDefault. default выбирается, поскольку имеет приоритет (потому что он определен в соответствующем подклассе признака, который определяет overrideDefault). В результате T должен быть привязан к Node.

  • find вызывается с параметром типа не Node (например, find[MyObj]("name")). В этом случае должен быть задан объект типа DefaultsTo[MyObj, Node]. Только метод overrideDefault может предоставить его, поэтому компилятор вставляет соответствующий вызов.

  • find вызывается с Node как параметр типа. Опять же, любой метод применим, но default выигрывает из-за его более высокого приоритета.

Ответ 2

Miles Sabin разместил действительно хорошее решение для этой проблемы в списке рассылки scala -user. Определите класс типа NotNothing следующим образом:

sealed trait NotNothing[T] { type U }                                          
object NotNothing {
   implicit val nothingIsNothing = new NotNothing[Nothing] { type U = Any }
   implicit def notNothing[T] = new NotNothing[T] { type U = T }           
}

Теперь вы можете определить свой Finder как

object Finder {
   def find[T <: Node : NotNothing](name: String): T = 
      doFind(name).asInstanceOf[T]
}

Если вы попытаетесь вызвать Finder.find без параметра типа, вы получите ошибку времени компиляции:

ошибка: неоднозначные неявные значения:  оба метода notNothing в объекте $iw типа [T] java.lang.Object с NotNothing [T] {тип U = T}  и значение nothingIsNothing в объекте $iw типа = > java.lang.Object с NotNothing [Nothing] {type U = Any}  соответствовать ожидаемому типу NotNothing [T]        Finder.find( "Джо" )

Это решение гораздо более общее, чем те, которые предложены в моих других ответах. Единственный недостаток, который я вижу, заключается в том, что ошибка времени компиляции довольно непрозрачна, а аннотация @implicitNotFound не помогает.

Ответ 3

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

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

object Finder {
   def find[T <: Node] = new Find[T]

   class Find[T <: Node] {
       def apply[R](name: String)(implicit e: T ReturnAs R): R = 
          doFind(name).asInstanceOf[R]
   }

   sealed class ReturnAs[T, R]
   trait DefaultReturns {
      implicit def returnAs[T] = new ReturnAs[T, T]
   }
   object ReturnAs extends DefaultReturns {
      implicit object returnNothingAsNode extends ReturnAs[Nothing, Node]
   }
}

Здесь метод find возвращает полиморфный функтор, который при применении к имени возвращает объект любого типа T или типа Node в зависимости от значения аргумента ReturnAs, предоставленного компилятор. Если T - Nothing, компилятор предоставит объект returnNothingAsNode, и метод apply вернет Node. В противном случае компилятор будет поставлять ReturnAs[T, T], а метод apply вернет T.


Устранение решения Paul в списке рассылки, другая возможность заключается в предоставлении неявного для каждого типа, который "работает". Вместо возврата Node, когда параметр типа опущен, будет выдана ошибка компиляции:

object Finder {
   def find[T : IsOk](name: String): T = 
      doFind(name).asInstanceOf[T]

   class IsOk[T]
   object IsOk {
      implicit object personIsOk extends IsOk[Person]
      implicit object nodeIsOk extends IsOk[Node]
   }
}

Конечно, это решение плохо масштабируется.

Ответ 4

Решение Paul обеспечивает нижнюю границу T, поэтому val person = find ( "joe" ) является ошибкой времени компиляции, заставляя вас явно указывать тип (например, Node). Но это довольно ужасное решение (Пол явно сказал, что он не рекомендует его), поскольку он требует, чтобы вы перечисляли все ваши подтипы.