Обеспечить разницу типов

В 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 отличались?

7 ответов

Решение

Вопреки идеям Жан-Филиппа, это работает:

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]
}

У меня есть более простое решение, которое также использует двусмысленность,

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")
                            ^

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

Примером со следующим определением:

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]
           ^

Так-то лучше. Спасибо компилятору.

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

type IsNot[A] = { type λ[B] = A =!= B }

Тогда мы можем определить f как это:

def f[T:IsNot[String]#λ] {}

Легче ли читать, очень субъективно. В любом случае это определенно короче, чем написание полного списка неявных параметров.

ОБНОВЛЕНИЕ: для полноты, здесь эквивалентный код для выражения этого 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 }

Основываясь на 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)

Вот еще одна попытка:

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])и там мы можем предотвратить этот обман, создав исключение.

Как тогда что-то вроде этого?

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 просто дать другую подпись первым двум методам.)

Это не ответ, просто начало того, что я мог бы подумать, является ответом. Код ниже вернет либо 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)
Другие вопросы по тегам