Использовать разницу типов
В Scala я могу обеспечить соблюдение равенства типов во время компиляции. Например:
case class Foo[A,B]( a: A, b: B )( implicit ev: A =:= B )
scala> Foo( 1, 2 )
res3: Foo[Int,Int] = Foo(1,2)
scala> Foo( 1, "2" )
<console>:10: error: Cannot prove that Int =:= java.lang.String.
Есть ли способ обеспечить, чтобы тип A и тип B были разными?
Ответы
Ответ 1
Устранение идей Жана-Филиппа, это работает:
sealed class =!=[A,B]
trait LowerPriorityImplicits {
implicit def equal[A]: =!=[A, A] = sys.error("should not be called")
}
object =!= extends LowerPriorityImplicits {
implicit def nequal[A,B](implicit same: A =:= B = null): =!=[A,B] =
if (same != null) sys.error("should not be called explicitly with same type")
else new =!=[A,B]
}
case class Foo[A,B](a: A, b: B)(implicit e: A =!= B)
Тогда:
// compiles:
Foo(1f, 1.0)
Foo("", 1.0)
Foo("", 1)
Foo("Fish", Some("Fish"))
// doesn't compile
// Foo(1f, 1f)
// Foo("", "")
Я бы, вероятно, упростил это следующим образом, так как проверки на "обман" всегда можно обойти (например, Foo(1, 1)(null)
или =!=.nequal(null)
):
sealed class =!=[A,B]
trait LowerPriorityImplicits {
/** do not call explicitly! */
implicit def equal[A]: =!=[A, A] = sys.error("should not be called")
}
object =!= extends LowerPriorityImplicits {
/** do not call explicitly! */
implicit def nequal[A,B]: =!=[A,B] = new =!=[A,B]
}
Ответ 2
У меня есть более простое решение, которое также использует двусмысленность,
trait =!=[A, B]
implicit def neq[A, B] : A =!= B = null
// This pair excludes the A =:= B case
implicit def neqAmbig1[A] : A =!= A = null
implicit def neqAmbig2[A] : A =!= A = null
Исходный вариант использования,
case class Foo[A,B](a : A, b : B)(implicit ev: A =!= B)
new Foo(1, "1")
new Foo("foo", Some("foo"))
// These don't compile
// new Foo(1, 1)
// new Foo("foo", "foo")
// new Foo(Some("foo"), Some("foo"))
Обновление
Мы можем связать это с моими "магическими трюками системы" (спасибо @jpp;-) следующим образом,
type ¬[T] = T => Nothing
implicit def neg[T, U](t : T)(implicit ev : T =!= U) : ¬[U] = null
def notString[T <% ¬[String]](t : T) = t
Пример сеанса REPL,
scala> val ns1 = notString(1)
ns1: Int = 1
scala> val ns2 = notString(1.0)
ns2: Double = 1.0
scala> val ns3 = notString(Some("foo"))
ns3: Some[java.lang.String] = Some(foo)
scala> val ns4 = notString("foo")
<console>:14: error: No implicit view available from
java.lang.String => (String) => Nothing.
val ns4 = notString2("foo")
^
Ответ 3
Мне понравилась простота и эффективность первого решения Майлза Сабина, но он был немного недоволен тем, что ошибка, которую мы получаем, не очень полезна:
Например, со следующим определением:
def f[T]( implicit e: T =!= String ) {}
При попытке сделать f[String]
невозможно скомпилировать с помощью:
<console>:10: error: ambiguous implicit values:
both method neqAmbig1 in object =!= of type [A]=> =!=[A,A]
and method neqAmbig2 in object =!= of type [A]=> =!=[A,A]
match expected type =!=[String,String]
f[String]
^
Я бы предпочел, чтобы компилятор мне что-то сказал по строке "T не отличается от String"
Оказывается, это довольно легко, если добавить еще один уровень имплицитов таким образом, что мы превратим ошибку неоднозначности
в неявную не найденную ошибку. С этого момента мы можем использовать аннотацию implicitNotFound
, чтобы исправить собственное сообщение об ошибке:
@annotation.implicitNotFound(msg = "Cannot prove that ${A} =!= ${B}.")
trait =!=[A,B]
object =!= {
class Impl[A, B]
object Impl {
implicit def neq[A, B] : A Impl B = null
implicit def neqAmbig1[A] : A Impl A = null
implicit def neqAmbig2[A] : A Impl A = null
}
implicit def foo[A,B]( implicit e: A Impl B ): A =!= B = null
}
Теперь попробуйте вызвать f[String]
:
scala> f[String]
<console>:10: error: Cannot prove that String =!= String.
f[String]
^
Это лучше. Спасибо компилятору.
В качестве последнего трюка для тех, которые похожи на связанный с контекстом синтаксический сахар, можно определить этот псевдоним (на основе типа lambdas):
type IsNot[A] = { type λ[B] = A =!= B }
Тогда мы можем определить f
следующим образом:
def f[T:IsNot[String]#λ] {}
Легче читать, это очень субъективно. В любом случае определенно короче, чем запись полного неявного списка параметров.
UPDATE. Для полноты здесь эквивалентный код для выражения, что A
не является подтипом B
:
@annotation.implicitNotFound(msg = "Cannot prove that ${A} <:!< ${B}.")
trait <:!<[A,B]
object <:!< {
class Impl[A, B]
object Impl {
implicit def nsub[A, B] : A Impl B = null
implicit def nsubAmbig1[A, B>:A] : A Impl B = null
implicit def nsubAmbig2[A, B>:A] : A Impl B = null
}
implicit def foo[A,B]( implicit e: A Impl B ): A <:!< B = null
}
type IsNotSub[B] = { type λ[A] = A <:!< B }
И для выражения, что A
не конвертируется в B
:
@annotation.implicitNotFound(msg = "Cannot prove that ${A} <%!< ${B}.")
trait <%!<[A,B]
object <%!< {
class Impl[A, B]
object Impl {
implicit def nconv[A, B] : A Impl B = null
implicit def nconvAmbig1[A<%B, B] : A Impl B = null
implicit def nconvAmbig2[A<%B, B] : A Impl B = null
}
implicit def foo[A,B]( implicit e: A Impl B ): A <%!< B = null
}
type IsNotView[B] = { type λ[A] = A <%!< B }
Ответ 4
Основываясь на Landei, кажется, что работает следующее:
case class Foo[A, B <: A, C <: A]( a: B, b: C)(implicit f: AnyVal <:< A)
scala> Foo(1f, 1.0)
res75: Foo[AnyVal,Float,Double] = Foo(1.0,1.0)
scala> Foo("", 1.0)
res76: Foo[Any,java.lang.String,Double] = Foo(,1.0)
scala> Foo(1f, 1f)
<console>:10: error: Cannot prove that AnyVal <:< Float.
Foo(1f, 1f)
^
scala> Foo("", "")
<console>:10: error: Cannot prove that AnyVal <:< java.lang.String.
Foo("", "")
^
scala> Foo("", 1)
res79: Foo[Any,java.lang.String,Int] = Foo(,1)
Ответ 5
Здесь другая попытка:
class =!=[A, B] private () extends NotNull
object =!= {
implicit def notMeantToBeCalled1[A, B >: A, C >: B <: A]: =!=[B, A] = error("should not be called")
implicit def notMeantToBeCalled2[A, B >: A, C >: B <: A]: =!=[B, A] = error("should not be called")
implicit def unambigouslyDifferent[A, B](implicit same: A =:= B = null): =!=[A, B] =
if (same != null) error("should not be called explicitly with the same type")
else new =!=
}
case class Foo[A, B](a: A, b: B)(implicit ev: A =!= B)
Затем снова:
// compiles:
Foo(1f, 1.0)
Foo("", 1.0)
Foo("", 1)
Foo("Fish", Some("Fish"))
// doesn't compile
// Foo(1f, 1f)
// Foo("", "")
Как и в моем другом предложении, целью здесь является введение неоднозначности времени компиляции, когда A
и B
совпадают. Здесь мы приводим два значения для случая, когда A
совпадает с B
, и однозначно подразумевается, когда это не так.
Обратите внимание, что проблема заключается в том, что вы все равно можете явно указать неявный параметр, вызвав вручную =!=.notMeantToBeCalled1
или =!=.unambigouslyDifferent
. Я не мог придумать способ предотвратить это во время компиляции. Однако мы можем исключить исключение во время выполнения, трюк, который unambigouslyDifferent
требует самого параметра доказательства, указывающего, что A
совпадает с B
. Но подождите... Разве мы не пытаемся доказать полную противоположность? Да, и почему этот неявный параметр same
имеет значение по умолчанию null
. И мы ожидаем, что это будет null
для всех юридических целей - единственный раз, когда он не был бы null
, когда противный пользователь звонит, например. Foo(1f, 1f)(=:=.unambiguouslyDifferent[Float, Float])
, и там мы можем предотвратить это обман, выбросив исключение.
Ответ 6
Как насчет чего-то подобного?
class Foo[A, B] private (a: A, b: B)
object Foo {
def apply[A, B <: A, C >: A <: B](a: A, b: B)(implicit nothing: Nothing) = nothing
def apply[A, B >: A, C >: B <: A](a: A, b: B)(implicit nothing: Nothing, dummy: DummyImplicit) = nothing
def apply[A, B](a: A, b: B): Foo[A, B] = new Foo(a, b)
}
Тогда:
// compiles:
Foo(1f, 1.0)
Foo("", 1.0)
Foo("", 1)
Foo("Fish", Some("Fish"))
// doesn't compile
// Foo(1f, 1f)
// Foo("", "")
Идея состоит в том, чтобы сделать разрешение неоднозначным, если A
совпадает с B
и недвусмысленно, если они не совпадают. Чтобы еще раз подчеркнуть, что неоднозначные методы не следует вызывать, я добавил неявный тип Nothing
, который никогда не должен быть вокруг (и, безусловно, должен выглядеть неправильно для вызывающего, если они пытаются вставить его явно). (Роль DummyImplicit
заключается в том, чтобы дать другую подпись первым двум методам.)
Ответ 7
Это не ответ, только начало того, что я думаю, является ответом. Приведенный ниже код вернет либо Yes
, либо No
в зависимости от того, являются ли типы равными или нет, если вы запрашиваете implicitly[AreEqual[A,B]]
. Как идти оттуда, чтобы сделать чек, я не смог понять. Может быть, весь подход обречен, может быть, кто-то может что-то сделать. Имейте в виду, implicitly[No[A, B]]
всегда что-то возвратит, нельзя использовать это.: - (
class AreEqual[A, B]
trait LowerPriorityImplicits {
implicit def toNo[A : Manifest, B : Manifest]: No[A, B] = No[A, B]
}
object AreEqual extends LowerPriorityImplicits {
implicit def toYes[A, B](implicit ev: A =:= B, m1: Manifest[A], m2: Manifest[B]): Yes[A, B] = Yes[A, B]
}
case class Yes[A : Manifest, B : Manifest]() extends AreEqual[A, B] {
override def toString: String = "Yes(%s, %s)" format (manifest[A].toString, manifest[B].toString)
}
case class No[A : Manifest, B : Manifest]() extends AreEqual[A, B] {
override def toString: String = "No(%s, %s)" format (manifest[A].toString, manifest[B].toString)
}
Тест:
scala> implicitly[AreEqual[String, Option[String]]]
res0: AreEqual[String,Option[String]] = No(java.lang.String, scala.Option[java.lang.String])
scala> implicitly[AreEqual[String, String]]
res1: AreEqual[String,String] = Yes(java.lang.String, java.lang.String)