Enforce type difference

2019-01-01 00:40发布

In Scala I can enforce type equality at compile time. For example:

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.

Is there a way to enforce that type A and type B should be different ?

标签: scala types
7条回答
何处买醉
2楼-- · 2019-01-01 00:59

How about something like this, then?

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

Then:

// compiles:
Foo(1f, 1.0)
Foo("", 1.0)
Foo("", 1)
Foo("Fish", Some("Fish"))

// doesn't compile
// Foo(1f, 1f)
// Foo("", "")

The idea is to make resolution ambiguous when Ais the same as B, and unambiguous when they are not the same. To further emphasize that the ambiguous methods should not be called, I added an implicit of type Nothing, which should never be around (and should certainly look wrong to the caller if they try to insert one explicitly). (The role of the DummyImplicit is just to give a different signature to the first two methods.)

查看更多
浪荡孟婆
3楼-- · 2019-01-01 01:08

I liked the simplicity and effectiveness of Miles Sabin's first solution, but was a bit dissatisfied with the fact that the error we get is not very helpful:

By example with the following definition:

def f[T]( implicit e: T =!= String ) {}

Attemtping to do f[String] will fail to compile with:

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

I'd rather have the compiler tell me something along the line of "T is not different from String" It turns out that it's quite easy if add yet another level of implicits in such a way that we turn the ambiguity error into an implicit not found error. From then we can use the implicitNotFound annotation to emit a custom error message:

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

Now let's try to call f[String]:

scala> f[String]
<console>:10: error: Cannot prove that String =!= String.
              f[String]
           ^

That's better. Thanks compiler.

As a last trick for those that like the context bound syntactic sugar, one can define this alias (based on type lambdas):

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

Then we can define f like this:

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

Whether it is easier to read is highly subjective. In any case is definitly shorter than writing the full implicit parameter list.

UPDATE: For completeness, here the equivalent code for expressing that A is is not a sub-type of 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 }

And for expressing that A is not convertible to 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楼-- · 2019-01-01 01:09

Riffing off of Jean-Philippe's ideas, this works:

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)

Then:

// compiles:
Foo(1f, 1.0)
Foo("", 1.0)
Foo("", 1)
Foo("Fish", Some("Fish"))

// doesn't compile
// Foo(1f, 1f)
// Foo("", "")

I'd probably simplify this as follows, since the checks for "cheating" can always be circumvented anyway (e.g. Foo(1, 1)(null) or =!=.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]
}
查看更多
余欢
5楼-- · 2019-01-01 01:16

Here's another attempt:

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)

Then, again:

// compiles:
Foo(1f, 1.0)
Foo("", 1.0)
Foo("", 1)
Foo("Fish", Some("Fish"))

// doesn't compile
// Foo(1f, 1f)
// Foo("", "")

Like in my other proposal, the aim here is to introduce a compile-time ambiguity when A and B are the same. Here, we provide two implicits for the case where A is the same as B, and an unambiguous implicit when this is not the case.

Note that the problem is that you could still explicitly provide the implicit parameter by manually calling =!=.notMeantToBeCalled1 or =!=.unambigouslyDifferent. I couldn't think of a way to prevent this at compile time. However, we can throw an exception at runtime, with the trick that unambigouslyDifferent requires an evidence parameter itself indicating whether A is the same as B. But wait... Aren't we trying to prove the exact opposite? Yes, and that's why that same implicit parameter has a default value of null. And we expect it to be null for all legal uses — the only time where it would not be null is when a nasty user calls e.g. Foo(1f, 1f)(=:=.unambiguouslyDifferent[Float, Float]), and there we can prevent this cheating by throwing an exception.

查看更多
无色无味的生活
6楼-- · 2019-01-01 01:17

Based on Landei's idea, the following seems to work:

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)
查看更多
只若初见
7楼-- · 2019-01-01 01:21

I have a simpler solution, which also leverages ambiguity,

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

The original use case,

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

Update

We can relate this to my "magical typesystem tricks" (thanks @jpp ;-) as follows,

type ¬[T] = T => Nothing
implicit def neg[T, U](t : T)(implicit ev : T =!= U) : ¬[U] = null

def notString[T <% ¬[String]](t : T) = t

Sample REPL session,

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")
                            ^
查看更多
登录 后发表回答