Using Scala Implicitly for Type Equality

时间秒杀一切 提交于 2019-11-30 19:15:12

X =:= Y is just syntactic sugar (infix notation) for the type =:=[X, Y].

So when you do implicitly[Y =:= Y], you are simply looking up an implicit value of type =:=[X, Y]. =:= is a generic trait defined in Predef.

Also, =:= is a valid type name because type names (just like any identifier) can contain special characters.

From now on, let's rename =:= as IsSameType and remove the infix notation so as to make our code more legible and straightforward. This gives us implicitly[IsSameType[X,Y]]

Here is a simplified version of how this type is defined:

sealed abstract class IsSameType[X, Y]
object IsSameType {
   implicit def tpEquals[A] = new IsSameType[A, A]{}
}

Notice how tpEquals provides an implicit value of IsSameType[A, A] for any type A. In other words, it provides an implicit value of IsSameType[X, Y] if and only if X and Y are the same type. So implicitly[IsSameType[Foo, Foo]] compiles fine. But implicitly[IsSameType[Int, String]] does not, as there is no implicit in scope of type IsSameType[Int, String], given that tpEquals is unapplicable here.

So with this very simple construct we are able to statically check that some type X is the same as another type Y.


Now here is an example of how it might be useful. Say I want to define a Pair type (ignoring the fact that it already exists in the standard library):

case class Pair[X,Y]( x: X, y: Y ) {
  def swap: Pair[Y,X] = Pair( y, x )
}

Pair is parameterized with the types of its 2 elements, which can be anything, and most importantly are unrelated. Now what if I want to define a method toList that converts the pair to a 2 elements list? This method would only really make sense in the case where X and Y are the same, otherwise I would be forced to return a List[Any]. And I certainly don't want to change the definition of Pair to Pair[T]( x: T, y: T ) because I really want to be able to have pairs of heterogeneous types. After all, it is only when calling toList that I need that X == Y. All other methods (such as swap) should be callable on any kind of heterogenous pair. So in the end I really want to statically ensure that X == Y, but only when calling toList, in which case it becomes possible and consistent to return a List[X] (or a List[Y], which is the same thing):

case class Pair[X,Y]( x: X, y: Y ) {
  def swap: Pair[Y,X] = Pair( y, x )
  def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = ???
}

But there is still a serious problem when it comes to actually implement toList. If I try to write the obvious implementation, this fails to compile:

def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = List[Y]( x, y )

The compiler will complain that x is not of type Y. And indeed, X and Y are still different types as far as the compiler is concerned. It is only by careful construction that we can be statically sure that X == Y (namely, the fact that toList takes an implicit value of type IsSameType[X, Y], and that they are provided by the method tpEquals only if X == Y). But the compiler certainly won't decipher this astute construction to conclude that X == Y.

What we can do to fix this situation is to provide an implicit conversion from X to Y provided that we know that X == Y (or in other words, that we have an instance of IsSameType[X, Y] in scope).

// A simple cast will do, given that we statically know that X == Y
implicit def sameTypeConvert[X,Y]( x: X )( implicit evidence: IsSameType[X, Y] ): Y = x.asInstanceOf[Y]

And now, our implementation of toList finally compiles fine: x will simply be converted to Y through the implicit conversion sameTypeConvert.

As a final tweak, we can simplify things even further: given that we are taking an implicit value (evidence) as a parameter already, why not have THIS value implement the conversion? Like this:

sealed abstract class IsSameType[X, Y] extends (X => Y) {
  def apply( x: X ): Y = x.asInstanceOf[Y]
}
object IsSameType {
   implicit def tpEquals[A] = new IsSameType[A, A]{}
}    

We can then remove the method sameTypeConvert, as the implicit conversion is now provided by the IsSameType instance itself. Now IsSameType serves a double purpose: statically ensuring that X == Y, and (if it is) providing the implicit conversion that actually allows us to treat instances of X as instances of Y.

We have now basically reimplemented the type =:= as defined in Predef

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!